mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Compare commits
242 Commits
v3.5.2
...
feat/rever
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c87964e16 | ||
|
|
776859c58e | ||
|
|
cb0fc82644 | ||
|
|
a625541125 | ||
|
|
300cff2be6 | ||
|
|
4f5c345c42 | ||
|
|
d513377f1c | ||
|
|
0df5486fec | ||
|
|
26dfb6db8e | ||
|
|
ae47498139 | ||
|
|
bc5c5da703 | ||
|
|
5f1026fcd2 | ||
|
|
de4b039d10 | ||
|
|
fd4d293e13 | ||
|
|
e5646240ab | ||
|
|
655de1361b | ||
|
|
ceb236c40c | ||
|
|
570b881c2b | ||
|
|
959bed746b | ||
|
|
f0cb2f9579 | ||
|
|
0f3d6ef76f | ||
|
|
3301f845f5 | ||
|
|
9689c19863 | ||
|
|
bd053a1635 | ||
|
|
a39f9ebb70 | ||
|
|
7e22344f77 | ||
|
|
96d6b90356 | ||
|
|
a87e04a366 | ||
|
|
bd86321d02 | ||
|
|
95fca086ba | ||
|
|
3e93a14deb | ||
|
|
d4b4b25ec8 | ||
|
|
2bed7c0b37 | ||
|
|
f3b4898e9c | ||
|
|
046f6d388a | ||
|
|
dfd640ebda | ||
|
|
88ed335fd4 | ||
|
|
d48f28b3c7 | ||
|
|
8f30f8a4ad | ||
|
|
7bbca91d9f | ||
|
|
dceb3a7509 | ||
|
|
d78631a7f4 | ||
|
|
889f3bb773 | ||
|
|
b62f8c13fc | ||
|
|
f552b759cb | ||
|
|
83e7f676bf | ||
|
|
cb25a55118 | ||
|
|
5d4409525c | ||
|
|
92af5c2b23 | ||
|
|
2cebc73789 | ||
|
|
e96cb3228e | ||
|
|
a71293148d | ||
|
|
aa152385b1 | ||
|
|
b6be03cea3 | ||
|
|
f027f05e02 | ||
|
|
56391837cd | ||
|
|
31bd523038 | ||
|
|
fd1f13225b | ||
|
|
dba4b2c4be | ||
|
|
05f10fd80b | ||
|
|
d59315e322 | ||
|
|
a29f2ba741 | ||
|
|
d33b0099a1 | ||
|
|
cad50649aa | ||
|
|
b3148855bc | ||
|
|
f56a3ea126 | ||
|
|
eea701f046 | ||
|
|
355d38c0a2 | ||
|
|
366da7839f | ||
|
|
a869902cfb | ||
|
|
f9d51ed33d | ||
|
|
092af7e421 | ||
|
|
33c15baf0e | ||
|
|
eeed34adcb | ||
|
|
393a04d7c3 | ||
|
|
de4119988c | ||
|
|
e0496b6449 | ||
|
|
607bc6407c | ||
|
|
de70beb01e | ||
|
|
9f4fd0ce61 | ||
|
|
bf9e3de0b5 | ||
|
|
9ce1c40b21 | ||
|
|
78358d212b | ||
|
|
ea792d0409 | ||
|
|
04be5c754e | ||
|
|
ab163ffa39 | ||
|
|
501ca7190e | ||
|
|
14fc764328 | ||
|
|
672ddff9f8 | ||
|
|
f4e84e46cc | ||
|
|
3409b53268 | ||
|
|
7e59e02d03 | ||
|
|
a7e4e21167 | ||
|
|
b22e082781 | ||
|
|
c056feb26f | ||
|
|
240b0705d5 | ||
|
|
2604dfea22 | ||
|
|
0adea20565 | ||
|
|
f182c7c8e9 | ||
|
|
d9379fbcb9 | ||
|
|
adc4af731e | ||
|
|
af1b3d4535 | ||
|
|
291935a1bc | ||
|
|
665365abdc | ||
|
|
86da92aa3f | ||
|
|
6ff694c078 | ||
|
|
a546b906e9 | ||
|
|
85b40b9a7e | ||
|
|
302ed92018 | ||
|
|
7e5cef6e44 | ||
|
|
01ac5239dc | ||
|
|
1dc2176813 | ||
|
|
a7ec632a21 | ||
|
|
2b6d8eba78 | ||
|
|
6bc6a10d66 | ||
|
|
51f8b12e13 | ||
|
|
957a6b512e | ||
|
|
8bc1c1c58c | ||
|
|
fbb67af1f6 | ||
|
|
4211f97dd0 | ||
|
|
f569b142f3 | ||
|
|
18e4baec3d | ||
|
|
b7199f4d95 | ||
|
|
2b8cc506df | ||
|
|
e0c5f80b68 | ||
|
|
63374ccd74 | ||
|
|
8eeacf478d | ||
|
|
87a4c27977 | ||
|
|
8177c3fd0c | ||
|
|
d28a5b828f | ||
|
|
68288adef3 | ||
|
|
51894d9924 | ||
|
|
2107261f7b | ||
|
|
737dc37433 | ||
|
|
6dcc6bfe56 | ||
|
|
714b30d6da | ||
|
|
e089ca8705 | ||
|
|
bf8fd0dacf | ||
|
|
6a94c3c812 | ||
|
|
c9b289cbde | ||
|
|
45b822ba10 | ||
|
|
cb758cceda | ||
|
|
8b804271bd | ||
|
|
57813784d2 | ||
|
|
7cbb8a00c4 | ||
|
|
4a4cb57348 | ||
|
|
7a82467933 | ||
|
|
6a65d191af | ||
|
|
d047d7a105 | ||
|
|
f05d0c2047 | ||
|
|
98e5b70f2e | ||
|
|
100ddd79aa | ||
|
|
0b2d4e2ba0 | ||
|
|
0c379d6c49 | ||
|
|
8e6f9fdb00 | ||
|
|
6b1713e54b | ||
|
|
44db5f9813 | ||
|
|
7d0fbbd960 | ||
|
|
4552256038 | ||
|
|
c5be5e94e8 | ||
|
|
3a6693c8b1 | ||
|
|
c7ecd541d5 | ||
|
|
e9032ae6e4 | ||
|
|
7202f740d3 | ||
|
|
2a74526b0f | ||
|
|
bacf6804e4 | ||
|
|
c60584c613 | ||
|
|
2dc700188d | ||
|
|
07ac13f4cc | ||
|
|
de605ecd02 | ||
|
|
81654d7688 | ||
|
|
14a8336695 | ||
|
|
e2482a6c8f | ||
|
|
27cbe755bf | ||
|
|
046debffa0 | ||
|
|
4b32015a73 | ||
|
|
4d950b30fb | ||
|
|
f50f1e51e4 | ||
|
|
97a414e77f | ||
|
|
46a6c8c987 | ||
|
|
5305e5ca18 | ||
|
|
6757ebe13c | ||
|
|
1dc91562ec | ||
|
|
2e2785c33c | ||
|
|
cf14f5a758 | ||
|
|
2ebd43d55c | ||
|
|
dac29767bd | ||
|
|
4571c7cf33 | ||
|
|
b8e3756dd9 | ||
|
|
df92852123 | ||
|
|
d6afcff5d2 | ||
|
|
9e9adfcc07 | ||
|
|
7c425eeccc | ||
|
|
b55e33f61a | ||
|
|
358c15163a | ||
|
|
0225e31947 | ||
|
|
d2b3508b5b | ||
|
|
1c6bc49ed0 | ||
|
|
e2ecfa3e3c | ||
|
|
875ef47938 | ||
|
|
550ac383e9 | ||
|
|
7a196e4315 | ||
|
|
8a6ed411ef | ||
|
|
4893d61ee3 | ||
|
|
a8482b2311 | ||
|
|
e734b6937a | ||
|
|
eab9f92b3e | ||
|
|
772aea69a9 | ||
|
|
f9d5cfce73 | ||
|
|
33cad40d5c | ||
|
|
ab7f29099d | ||
|
|
70d63934a6 | ||
|
|
61dae4cf84 | ||
|
|
022673322b | ||
|
|
9e321e9c69 | ||
|
|
0fc46236fc | ||
|
|
91c03ef5a5 | ||
|
|
ba6d04ced7 | ||
|
|
d26635784f | ||
|
|
12fc2fc1fb | ||
|
|
05e8d53783 | ||
|
|
ae77444b2d | ||
|
|
88dae597ea | ||
|
|
4dd11db8f4 | ||
|
|
4ac8f1f246 | ||
|
|
72f031b0ae | ||
|
|
c8c492bf1a | ||
|
|
eb3c1a0c8b | ||
|
|
c8ad04b065 | ||
|
|
efbdaab27b | ||
|
|
5f7b6158a2 | ||
|
|
51931df2d2 | ||
|
|
2b8fd61c16 | ||
|
|
7c2a847024 | ||
|
|
659f811c09 | ||
|
|
2224b00c57 | ||
|
|
b56ed48855 | ||
|
|
d3ed8dee7c | ||
|
|
54a40d0ccc | ||
|
|
186837d9f9 | ||
|
|
ec3920fc34 | ||
|
|
5a3aa7e8c6 |
@@ -1,5 +1,5 @@
|
||||
MAIN_VITE_API_URL=API_URL
|
||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||
MAIN_VITE_API_URL=
|
||||
MAIN_VITE_AUTH_URL=
|
||||
MAIN_VITE_WS_URL=
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE=
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
os: [windows-2022, ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: yarn build:win
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
os: [windows-2022, ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: yarn build:win
|
||||
env:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "electron-vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
loadEnv(mode);
|
||||
@@ -48,15 +47,7 @@ export default defineConfig(({ mode }) => {
|
||||
"@shared": resolve("src/shared"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
svgr(),
|
||||
react(),
|
||||
sentryVitePlugin({
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: "hydra-launcher",
|
||||
project: "hydra-renderer",
|
||||
}),
|
||||
],
|
||||
plugins: [svgr(), react()],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.5.2",
|
||||
"version": "3.6.8",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -40,8 +40,6 @@
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
"@sentry/react": "^8.47.0",
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"axios-cookiejar-support": "^5.0.5",
|
||||
@@ -56,21 +54,26 @@
|
||||
"diskusage": "^1.2.0",
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-type": "^19.6.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-type": "^20.5.0",
|
||||
"framer-motion": "^12.15.0",
|
||||
"i18next": "^23.11.2",
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
"rc-virtual-list": "^3.16.1",
|
||||
"parse-torrent": "^11.0.18",
|
||||
"rc-virtual-list": "^3.18.3",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-redux": "^9.1.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-shadow": "^20.6.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"sound-play": "^1.1.0",
|
||||
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
@@ -106,9 +109,9 @@
|
||||
"@types/winreg": "^1.2.36",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"electron": "^31.7.7",
|
||||
"electron": "^32.3.3",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^2.3.0",
|
||||
"electron-vite": "^3.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
@@ -122,5 +125,6 @@
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -101,8 +101,13 @@ def process_list():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
iter_list = ['exe', 'pid', 'name']
|
||||
if sys.platform != 'win32':
|
||||
iter_list.append('cwd')
|
||||
iter_list.append('environ')
|
||||
|
||||
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'name'])]
|
||||
process_list = [proc.info for proc in psutil.process_iter(iter_list)]
|
||||
return jsonify(process_list), 200
|
||||
|
||||
@app.route("/profile-image", methods=["POST"])
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
||||
},
|
||||
"home": {
|
||||
"featured": "مميز",
|
||||
"surprise_me": "مفاجئني",
|
||||
"no_results": "لم يتم العثور على نتائج",
|
||||
"start_typing": "ابدأ بالكتابة للبحث...",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "беларуская мова",
|
||||
"home": {
|
||||
"featured": "Рэкамэндаванае",
|
||||
"surprise_me": "Здзіві мяне",
|
||||
"no_results": "Няма вынікаў"
|
||||
},
|
||||
@@ -17,7 +16,6 @@
|
||||
"home": "Галоўная",
|
||||
"favorites": "Улюбленыя"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
"home": "Галоўная",
|
||||
@@ -31,10 +29,7 @@
|
||||
"downloading_metadata": "Сцягванне мэтаданых {{title}}…",
|
||||
"downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Наступная старонка",
|
||||
"previous_page": "Папярэдняя старонка"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Адкрыць варыянты сцягвання",
|
||||
"download_options_zero": "Няма варыянтаў сцягвання",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Успешно влизане"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Препоръчани",
|
||||
"surprise_me": "Изненадай ме",
|
||||
"no_results": "Няма намерени резултати",
|
||||
"start_typing": "Започнете да пишете за търсене...",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Has entrat correctament"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destacats",
|
||||
"surprise_me": "Sorprèn-me",
|
||||
"no_results": "No s'ha trobat res"
|
||||
},
|
||||
@@ -25,7 +24,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Cerca jocs",
|
||||
|
||||
"home": "Inici",
|
||||
"catalogue": "Catàleg",
|
||||
"downloads": "Baixades",
|
||||
@@ -41,10 +39,7 @@
|
||||
"calculating_eta": "Descarregant {{title}}… ({{percentage}} completat) - Calculant el temps restant…",
|
||||
"checking_files": "Comprovant els fitxers de {{title}}… ({{percentage}} completat)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Pàgina següent",
|
||||
"previous_page": "Pàgina anterior"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Obre les opcions de baixada",
|
||||
"download_options_zero": "No hi ha opcions de baixada",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Úspěšně přihlášen"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Doporučené",
|
||||
"surprise_me": "Překvap mě",
|
||||
"no_results": "Výsledek nenalezen",
|
||||
"start_typing": "Začni psát pro vyhledávání...",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Loggede ind successfuldt"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Anbefalet",
|
||||
"surprise_me": "Overrask mig",
|
||||
"no_results": "Ingen resultater fundet",
|
||||
"start_typing": "Begynd at skrive for at søge...",
|
||||
@@ -29,7 +28,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Søg efter spil",
|
||||
|
||||
"home": "Hjem",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
@@ -45,10 +43,7 @@
|
||||
"calculating_eta": "Downloader {{title}}… ({{percentage}} færdig) - Udregner resterende tid…",
|
||||
"checking_files": "Checker {{title}} filer… ({{percentage}} færdig)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Næste side",
|
||||
"previous_page": "Forrige side"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Åben download muligheder",
|
||||
"download_options_zero": "Ingen download mulighed",
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
"successfully_signed_in": "Erfolgreich angemeldet"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Empfohlen",
|
||||
"surprise_me": "Überrasche mich",
|
||||
"no_results": "Keine Ergebnisse gefunden"
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"start_typing": "Tippe, um zu suchen...",
|
||||
"hot": "Jetzt beliebt",
|
||||
"weekly": "📅 Top-Spiele der Woche",
|
||||
"achievements": "🏆 Spiele zum Meistern"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
@@ -21,11 +24,13 @@
|
||||
"queued": "{{title}} (In Warteschlange)",
|
||||
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
||||
"sign_in": "Anmelden",
|
||||
"favorites": "Favoriten"
|
||||
"friends": "Freunde",
|
||||
"need_help": "Brauchst du Hilfe?",
|
||||
"favorites": "Favoriten",
|
||||
"playable_button_title": "Nur Spiele anzeigen, die du jetzt spielen kannst"
|
||||
},
|
||||
"header": {
|
||||
"search": "Spiele suchen",
|
||||
|
||||
"home": "Home",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
@@ -39,11 +44,21 @@
|
||||
"downloading_metadata": "Metadaten von {{title}} werden heruntergeladen…",
|
||||
"downloading": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Abschluss {{eta}} - {{speed}}",
|
||||
"calculating_eta": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Verbleibende Zeit wird berechnet…",
|
||||
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)"
|
||||
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Installation abgeschlossen",
|
||||
"installation_complete_message": "Allgemeine Redistributables erfolgreich installiert"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Nächste Seite",
|
||||
"previous_page": "Vorherige Seite"
|
||||
"search": "Filtern…",
|
||||
"developers": "Entwickler",
|
||||
"genres": "Genres",
|
||||
"tags": "Tags",
|
||||
"publishers": "Publisher",
|
||||
"download_sources": "Download-Quellen",
|
||||
"result_count": "{{resultCount}} Ergebnisse",
|
||||
"filter_count": "{{filterCount}} verfügbar",
|
||||
"clear_filters": "{{filterCount}} ausgewählte löschen"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Download-Optionen öffnen",
|
||||
@@ -101,6 +116,7 @@
|
||||
"open_folder": "Verzeichnis öffnen",
|
||||
"open_download_location": "Heruntergeladene Dateien anzeigen",
|
||||
"create_shortcut": "Desktop-Verknüpfung erstellen",
|
||||
"clear": "Löschen",
|
||||
"remove_files": "Dateien entfernen",
|
||||
"remove_from_library_title": "Bist du dir sicher?",
|
||||
"remove_from_library_description": "Dies wird {{game}} aus deiner Bibliothek entfernen",
|
||||
@@ -114,8 +130,81 @@
|
||||
"download_in_progress": "Download erfolgt",
|
||||
"download_paused": "Download ist pausiert",
|
||||
"last_downloaded_option": "Letzte Download-Option",
|
||||
"create_steam_shortcut": "Steam-Verknüpfung erstellen",
|
||||
"create_shortcut_success": "Verknüpfung erfolgreich erstellt",
|
||||
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung"
|
||||
"you_might_need_to_restart_steam": "Möglicherweise musst du Steam neu starten, um die Änderungen zu sehen",
|
||||
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung",
|
||||
"nsfw_content_title": "Dieses Spiel enthält unangemessene Inhalte",
|
||||
"nsfw_content_description": "{{title}} enthält Inhalte, die möglicherweise nicht für alle Altersgruppen geeignet sind. Bist du sicher, dass du fortfahren möchtest?",
|
||||
"allow_nsfw_content": "Fortfahren",
|
||||
"refuse_nsfw_content": "Zurück",
|
||||
"stats": "Statistiken",
|
||||
"download_count": "Downloads",
|
||||
"player_count": "Aktive Spieler",
|
||||
"download_error": "Diese Download-Option ist nicht verfügbar",
|
||||
"download": "Download",
|
||||
"executable_path_in_use": "Ausführbare Datei wird bereits von \"{{game}}\" verwendet",
|
||||
"warning": "Warnung:",
|
||||
"hydra_needs_to_remain_open": "Für diesen Download muss Hydra geöffnet bleiben, bis er abgeschlossen ist. Wenn Hydra vor Abschluss geschlossen wird, verlierst du deinen Fortschritt.",
|
||||
"achievements": "Erfolge",
|
||||
"achievements_count": "Erfolge {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Cloud-Speicherstand",
|
||||
"cloud_save_description": "Speichere deinen Fortschritt in der Cloud und spiele auf jedem Gerät weiter",
|
||||
"backups": "Sicherungen",
|
||||
"install_backup": "Installieren",
|
||||
"delete_backup": "Löschen",
|
||||
"create_backup": "Neue Sicherung",
|
||||
"last_backup_date": "Letzte Sicherung am {{date}}",
|
||||
"no_backup_preview": "Keine Spielstände für diesen Titel gefunden",
|
||||
"restoring_backup": "Sicherung wird wiederhergestellt ({{progress}} abgeschlossen)…",
|
||||
"uploading_backup": "Sicherung wird hochgeladen…",
|
||||
"no_backups": "Du hast noch keine Sicherungen für dieses Spiel erstellt",
|
||||
"backup_uploaded": "Sicherung hochgeladen",
|
||||
"backup_deleted": "Sicherung gelöscht",
|
||||
"backup_restored": "Sicherung wiederhergestellt",
|
||||
"see_all_achievements": "Alle Erfolge anzeigen",
|
||||
"sign_in_to_see_achievements": "Anmelden, um Erfolge zu sehen",
|
||||
"mapping_method_automatic": "Automatisch",
|
||||
"mapping_method_manual": "Manuell",
|
||||
"mapping_method_label": "Zuordnungsmethode",
|
||||
"files_automatically_mapped": "Dateien automatisch zugeordnet",
|
||||
"no_backups_created": "Keine Sicherungen für dieses Spiel erstellt",
|
||||
"manage_files": "Dateien verwalten",
|
||||
"loading_save_preview": "Suche nach Spielständen…",
|
||||
"wine_prefix": "Wine-Präfix",
|
||||
"wine_prefix_description": "Das Wine-Präfix, das zum Ausführen dieses Spiels verwendet wird",
|
||||
"launch_options": "Startoptionen",
|
||||
"launch_options_description": "Fortgeschrittene Benutzer können Modifikationen ihrer Startoptionen eingeben (experimentelle Funktion)",
|
||||
"launch_options_placeholder": "Kein Parameter angegeben",
|
||||
"no_download_option_info": "Keine Informationen verfügbar",
|
||||
"backup_deletion_failed": "Sicherung konnte nicht gelöscht werden",
|
||||
"max_number_of_artifacts_reached": "Maximale Anzahl von Sicherungen für dieses Spiel erreicht",
|
||||
"achievements_not_sync": "Sieh, wie du deine Erfolge synchronisieren kannst",
|
||||
"manage_files_description": "Verwalte, welche Dateien gesichert und wiederhergestellt werden",
|
||||
"select_folder": "Ordner auswählen",
|
||||
"backup_from": "Sicherung vom {{date}}",
|
||||
"automatic_backup_from": "Automatische Sicherung vom {{date}}",
|
||||
"enable_automatic_cloud_sync": "Automatische Cloud-Synchronisierung aktivieren",
|
||||
"custom_backup_location_set": "Benutzerdefinierter Sicherungsort festgelegt",
|
||||
"no_directory_selected": "Kein Verzeichnis ausgewählt",
|
||||
"no_write_permission": "Kann nicht in dieses Verzeichnis herunterladen. Klicke hier, um mehr zu erfahren.",
|
||||
"reset_achievements": "Erfolge zurücksetzen",
|
||||
"reset_achievements_description": "Dies wird alle Erfolge für {{game}} zurücksetzen",
|
||||
"reset_achievements_title": "Bist du dir sicher?",
|
||||
"reset_achievements_success": "Erfolge erfolgreich zurückgesetzt",
|
||||
"reset_achievements_error": "Fehler beim Zurücksetzen der Erfolge",
|
||||
"download_error_gofile_quota_exceeded": "Du hast dein monatliches Gofile-Kontingent überschritten. Bitte warte, bis das Kontingent zurückgesetzt wird.",
|
||||
"download_error_real_debrid_account_not_authorized": "Dein Real-Debrid-Konto ist nicht für neue Downloads autorisiert. Bitte überprüfe deine Kontoeinstellungen und versuche es erneut.",
|
||||
"download_error_not_cached_on_real_debrid": "Dieser Download ist nicht auf Real-Debrid verfügbar und das Abrufen des Download-Status von Real-Debrid ist noch nicht verfügbar.",
|
||||
"download_error_not_cached_on_torbox": "Dieser Download ist nicht auf TorBox verfügbar und das Abrufen des Download-Status von TorBox ist noch nicht verfügbar.",
|
||||
"download_error_not_cached_on_hydra": "Dieser Download ist nicht auf Nimbus verfügbar.",
|
||||
"game_removed_from_favorites": "Spiel aus Favoriten entfernt",
|
||||
"game_added_to_favorites": "Spiel zu Favoriten hinzugefügt",
|
||||
"automatically_extract_downloaded_files": "Heruntergeladene Dateien automatisch entpacken",
|
||||
"create_start_menu_shortcut": "Startmenü-Verknüpfung erstellen",
|
||||
"invalid_wine_prefix_path": "Ungültiger Wine-Präfix-Pfad",
|
||||
"invalid_wine_prefix_path_description": "Der Pfad zum Wine-Präfix ist ungültig. Bitte überprüfe den Pfad und versuche es erneut.",
|
||||
"missing_wine_prefix": "Wine-Präfix ist erforderlich, um eine Sicherung unter Linux zu erstellen"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra aktivieren",
|
||||
@@ -148,7 +237,13 @@
|
||||
"queued": "In Warteschlange",
|
||||
"no_downloads_title": "Welch Leere",
|
||||
"no_downloads_description": "Du hast mit Hydra noch nichts heruntergeladen, aber es ist nie zu spät anzufangen.",
|
||||
"checking_files": "Dateien werden überprüft…"
|
||||
"checking_files": "Dateien werden überprüft…",
|
||||
"seeding": "Seeding",
|
||||
"stop_seeding": "Seeding stoppen",
|
||||
"resume_seeding": "Seeding fortsetzen",
|
||||
"options": "Verwalten",
|
||||
"extract": "Dateien entpacken",
|
||||
"extracting": "Dateien werden entpackt…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Download-Pfad",
|
||||
@@ -185,11 +280,11 @@
|
||||
"download_source_errored": "Fehlgeschlagen",
|
||||
"sync_download_sources": "Quellen synchronisieren",
|
||||
"removed_download_source": "Download-Quelle entfernt",
|
||||
"removed_download_sources": "Download-Quellen entfernt",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nein",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, alles löschen",
|
||||
"description_confirmation_delete_all_sources": "Du löschen alle Downloadquellen",
|
||||
"title_confirmation_delete_all_sources": "Löschen du alle Downloadquellen",
|
||||
"removed_download_sources": "Download-Quellen entfernt",
|
||||
"title_confirmation_delete_all_sources": "Möchtest du alle Downloadquellen löschen",
|
||||
"description_confirmation_delete_all_sources": "Möchtest du alle Downloadquellen löschen",
|
||||
"button_delete_all_sources": "Entfernen Sie alle Downloadquellen",
|
||||
"added_download_source": "Download-Quelle hinzugefügt",
|
||||
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
|
||||
@@ -197,7 +292,95 @@
|
||||
"found_download_option_zero": "Keine Download-Option gefunden",
|
||||
"found_download_option_one": "{{countFormatted}} Download-Option gefunden",
|
||||
"found_download_option_other": "{{countFormatted}} Download-Optionen gefunden",
|
||||
"import": "Importieren"
|
||||
"import": "Importieren",
|
||||
"public": "Öffentlich",
|
||||
"private": "Privat",
|
||||
"friends_only": "Nur Freunde",
|
||||
"privacy": "Privatsphäre",
|
||||
"profile_visibility": "Profilsichtbarkeit",
|
||||
"profile_visibility_description": "Wähle, wer dein Profil und deine Bibliothek sehen kann",
|
||||
"required_field": "Dieses Feld ist erforderlich",
|
||||
"source_already_exists": "Diese Quelle wurde bereits hinzugefügt",
|
||||
"must_be_valid_url": "Die Quelle muss eine gültige URL sein",
|
||||
"blocked_users": "Blockierte Benutzer",
|
||||
"user_unblocked": "Benutzer wurde freigegeben",
|
||||
"enable_achievement_notifications": "Wenn ein Erfolg freigeschaltet wird",
|
||||
"launch_minimized": "Hydra minimiert starten",
|
||||
"disable_nsfw_alert": "NSFW-Warnung deaktivieren",
|
||||
"seed_after_download_complete": "Nach Download-Abschluss seeden",
|
||||
"show_hidden_achievement_description": "Versteckte Erfolgsbeschreibungen vor dem Freischalten anzeigen",
|
||||
"account": "Konto",
|
||||
"no_users_blocked": "Du hast keine blockierten Benutzer",
|
||||
"subscription_active_until": "Deine Hydra Cloud ist aktiv bis {{date}}",
|
||||
"manage_subscription": "Abonnement verwalten",
|
||||
"update_email": "E-Mail aktualisieren",
|
||||
"update_password": "Passwort aktualisieren",
|
||||
"current_email": "Aktuelle E-Mail:",
|
||||
"no_email_account": "Du hast noch keine E-Mail festgelegt",
|
||||
"account_data_updated_successfully": "Kontodaten erfolgreich aktualisiert",
|
||||
"renew_subscription": "Hydra Cloud erneuern",
|
||||
"subscription_expired_at": "Dein Abonnement ist am {{date}} abgelaufen",
|
||||
"no_subscription": "Genieße Hydra auf die bestmögliche Weise",
|
||||
"become_subscriber": "Werde Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Automatische Verlängerung ist deaktiviert",
|
||||
"subscription_renews_on": "Dein Abonnement verlängert sich am {{date}}",
|
||||
"bill_sent_until": "Deine nächste Rechnung wird bis zu diesem Tag gesendet",
|
||||
"no_themes": "Scheint, als hättest du noch keine Themes, aber keine Sorge, klicke hier, um dein erstes Meisterwerk zu erstellen.",
|
||||
"editor_tab_code": "Code",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Speichern",
|
||||
"web_store": "Web Store",
|
||||
"clear_themes": "Löschen",
|
||||
"create_theme": "Erstellen",
|
||||
"create_theme_modal_title": "Benutzerdefiniertes Theme erstellen",
|
||||
"create_theme_modal_description": "Erstelle ein neues Theme, um das Aussehen von Hydra anzupassen",
|
||||
"theme_name": "Name",
|
||||
"insert_theme_name": "Theme-Namen eingeben",
|
||||
"set_theme": "Theme festlegen",
|
||||
"unset_theme": "Theme entfernen",
|
||||
"delete_theme": "Theme löschen",
|
||||
"edit_theme": "Theme bearbeiten",
|
||||
"delete_all_themes": "Alle Themes löschen",
|
||||
"delete_all_themes_description": "Dies wird alle deine benutzerdefinierten Themes löschen",
|
||||
"delete_theme_description": "Dies wird das Theme {{theme}} löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"appearance": "Erscheinungsbild",
|
||||
"enable_torbox": "TorBox aktivieren",
|
||||
"torbox_description": "TorBox ist dein Premium-Seedbox-Service, der sogar mit den besten Servern auf dem Markt konkurriert.",
|
||||
"torbox_account_linked": "TorBox-Konto verknüpft",
|
||||
"create_real_debrid_account": "Klicke hier, wenn du noch kein Real-Debrid-Konto hast",
|
||||
"create_torbox_account": "Klicke hier, wenn du noch kein TorBox-Konto hast",
|
||||
"real_debrid_account_linked": "Real-Debrid-Konto verknüpft",
|
||||
"name_min_length": "Theme-Name muss mindestens 3 Zeichen lang sein",
|
||||
"import_theme": "Theme importieren",
|
||||
"import_theme_description": "Du wirst {{theme}} aus dem Theme Store importieren",
|
||||
"error_importing_theme": "Fehler beim Importieren des Themes",
|
||||
"theme_imported": "Theme erfolgreich importiert",
|
||||
"enable_friend_request_notifications": "Wenn eine Freundschaftsanfrage empfangen wird",
|
||||
"enable_auto_install": "Updates automatisch herunterladen",
|
||||
"common_redist": "Allgemeine Redistributables",
|
||||
"common_redist_description": "Allgemeine Redistributables sind erforderlich, um einige Spiele auszuführen. Es wird empfohlen, sie zu installieren, um Probleme zu vermeiden.",
|
||||
"install_common_redist": "Installieren",
|
||||
"installing_common_redist": "Installiere…",
|
||||
"show_download_speed_in_megabytes": "Download-Geschwindigkeit in Megabyte pro Sekunde anzeigen",
|
||||
"extract_files_by_default": "Dateien nach dem Download standardmäßig entpacken",
|
||||
"achievement_custom_notification_position": "Position der benutzerdefinierten Erfolgsbenachrichtigung",
|
||||
"top-left": "Oben links",
|
||||
"top-center": "Oben mittig",
|
||||
"top-right": "Oben rechts",
|
||||
"bottom-left": "Unten links",
|
||||
"bottom-center": "Unten mittig",
|
||||
"bottom-right": "Unten rechts",
|
||||
"enable_achievement_custom_notifications": "Benutzerdefinierte Erfolgsbenachrichtigungen aktivieren",
|
||||
"alignment": "Ausrichtung",
|
||||
"variation": "Variation",
|
||||
"default": "Standard",
|
||||
"rare": "Selten",
|
||||
"platinum": "Platin",
|
||||
"hidden": "Versteckt",
|
||||
"test_notification": "Testbenachrichtigung",
|
||||
"notification_preview": "Vorschau der Erfolgsbenachrichtigung",
|
||||
"enable_friend_start_game_notifications": "Wenn ein Freund ein Spiel startet"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download abgeschlossen",
|
||||
@@ -206,13 +389,24 @@
|
||||
"repack_count_one": "{{count}} Repack hinzugefügt",
|
||||
"repack_count_other": "{{count}} Repacks hinzugefügt",
|
||||
"new_update_available": "Version {{version}} verfügbar",
|
||||
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu"
|
||||
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu",
|
||||
"notification_achievement_unlocked_title": "Erfolg für {{game}} freigeschaltet",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} und {{count}} weitere wurden freigeschaltet",
|
||||
"new_friend_request_description": "{{displayName}} hat dir eine Freundschaftsanfrage gesendet",
|
||||
"new_friend_request_title": "Neue Freundschaftsanfrage",
|
||||
"extraction_complete": "Entpacken abgeschlossen",
|
||||
"game_extracted": "{{title}} erfolgreich entpackt",
|
||||
"friend_started_playing_game": "{{displayName}} hat begonnen, ein Spiel zu spielen",
|
||||
"test_achievement_notification_title": "Dies ist eine Testbenachrichtigung",
|
||||
"test_achievement_notification_description": "Ziemlich cool, oder?"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra öffnen",
|
||||
"quit": "Schließen"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Verfügbar",
|
||||
"available_other": "Verfügbar",
|
||||
"no_downloads": "Keine Downloads verfügbar"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -274,6 +468,66 @@
|
||||
"no_pending_invites": "Du hast keine ausstehenden Einladungen",
|
||||
"no_blocked_users": "Du hast keine blockierten Nutzer",
|
||||
"friend_code_copied": "Freundescode kopiert",
|
||||
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt"
|
||||
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt",
|
||||
"privacy_hint": "Um anzupassen, wer dies sehen kann, gehe zu den <0>Einstellungen</0>",
|
||||
"locked_profile": "Dieses Profil ist privat",
|
||||
"image_process_failure": "Fehler bei der Bildverarbeitung",
|
||||
"required_field": "Dieses Feld ist erforderlich",
|
||||
"displayname_min_length": "Anzeigename muss mindestens 3 Zeichen lang sein",
|
||||
"displayname_max_length": "Anzeigename darf maximal 50 Zeichen lang sein",
|
||||
"report_profile": "Dieses Profil melden",
|
||||
"report_reason": "Warum meldest du dieses Profil?",
|
||||
"report_description": "Zusätzliche Informationen",
|
||||
"report_description_placeholder": "Zusätzliche Informationen",
|
||||
"report": "Melden",
|
||||
"report_reason_hate": "Hassrede",
|
||||
"report_reason_sexual_content": "Sexuelle Inhalte",
|
||||
"report_reason_violence": "Gewalt",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Sonstiges",
|
||||
"profile_reported": "Profil gemeldet",
|
||||
"your_friend_code": "Dein Freundescode:",
|
||||
"upload_banner": "Banner hochladen",
|
||||
"uploading_banner": "Banner wird hochgeladen…",
|
||||
"background_image_updated": "Hintergrundbild aktualisiert",
|
||||
"stats": "Statistiken",
|
||||
"achievements": "Erfolge",
|
||||
"games": "Spiele",
|
||||
"top_percentile": "Top {{percentile}}%",
|
||||
"ranking_updated_weekly": "Rangliste wird wöchentlich aktualisiert",
|
||||
"playing": "Spielt {{game}}",
|
||||
"achievements_unlocked": "Erfolge freigeschaltet",
|
||||
"earned_points": "Verdiente Punkte",
|
||||
"show_achievements_on_profile": "Zeige deine Erfolge auf deinem Profil",
|
||||
"show_points_on_profile": "Zeige deine verdienten Punkte auf deinem Profil"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Erfolg freigeschaltet",
|
||||
"user_achievements": "{{displayName}}'s Erfolge",
|
||||
"your_achievements": "Deine Erfolge",
|
||||
"unlocked_at": "Freigeschaltet am: {{date}}",
|
||||
"subscription_needed": "Ein Hydra Cloud-Abonnement ist erforderlich, um diesen Inhalt zu sehen",
|
||||
"new_achievements_unlocked": "{{achievementCount}} neue Erfolge von {{gameCount}} Spielen freigeschaltet",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} Erfolge",
|
||||
"achievements_unlocked_for_game": "{{achievementCount}} neue Erfolge für {{gameTitle}} freigeschaltet",
|
||||
"hidden_achievement_tooltip": "Dies ist ein versteckter Erfolg",
|
||||
"achievement_earn_points": "Verdiene {{points}} Punkte mit diesem Erfolg",
|
||||
"earned_points": "Verdiente Punkte:",
|
||||
"available_points": "Verfügbare Punkte:",
|
||||
"how_to_earn_achievements_points": "Wie verdient man Erfolgspunkte?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud-Abonnement",
|
||||
"subscribe_now": "Jetzt abonnieren",
|
||||
"cloud_saving": "Cloud-Speicherung",
|
||||
"cloud_achievements": "Speichere deine Erfolge in der Cloud",
|
||||
"animated_profile_picture": "Animierte Profilbilder",
|
||||
"premium_support": "Premium-Support",
|
||||
"show_and_compare_achievements": "Zeige und vergleiche deine Erfolge mit anderen Nutzern",
|
||||
"animated_profile_banner": "Animiertes Profilbanner",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Du hast gerade eine Hydra Cloud-Funktion entdeckt!",
|
||||
"learn_more": "Mehr erfahren",
|
||||
"debrid_description": "Lade bis zu 4x schneller mit Nimbus herunter"
|
||||
}
|
||||
}
|
||||
|
||||
108
src/locales/en/translation.json
Normal file → Executable file
108
src/locales/en/translation.json
Normal file → Executable file
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Successfully signed in"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Featured",
|
||||
"surprise_me": "Surprise me",
|
||||
"no_results": "No results found",
|
||||
"start_typing": "Starting typing to search...",
|
||||
@@ -27,7 +26,57 @@
|
||||
"sign_in": "Sign in",
|
||||
"friends": "Friends",
|
||||
"need_help": "Need help?",
|
||||
"favorites": "Favorites"
|
||||
"favorites": "Favorites",
|
||||
"playable_button_title": "Show only games you can play now",
|
||||
"add_custom_game_tooltip": "Add Custom Game",
|
||||
"show_playable_only_tooltip": "Show Playable Only",
|
||||
"custom_game_modal": "Add Custom Game",
|
||||
"custom_game_modal_description": "Add a custom game to your library by selecting an executable file",
|
||||
"custom_game_modal_executable_path": "Executable Path",
|
||||
"custom_game_modal_select_executable": "Select executable file",
|
||||
"custom_game_modal_title": "Title",
|
||||
"custom_game_modal_enter_title": "Enter title",
|
||||
"custom_game_modal_browse": "Browse",
|
||||
"custom_game_modal_cancel": "Cancel",
|
||||
"custom_game_modal_add": "Add Game",
|
||||
"custom_game_modal_adding": "Adding Game...",
|
||||
"custom_game_modal_success": "Custom game added successfully",
|
||||
"custom_game_modal_failed": "Failed to add custom game",
|
||||
"custom_game_modal_executable": "Executable",
|
||||
"edit_game_modal": "Customize Assets",
|
||||
"edit_game_modal_description": "Customize game assets and details",
|
||||
"edit_game_modal_title": "Title",
|
||||
"edit_game_modal_enter_title": "Enter title",
|
||||
"edit_game_modal_image": "Image",
|
||||
"edit_game_modal_select_image": "Select image",
|
||||
"edit_game_modal_browse": "Browse",
|
||||
"edit_game_modal_image_preview": "Image preview",
|
||||
"edit_game_modal_icon": "Icon",
|
||||
"edit_game_modal_select_icon": "Select icon",
|
||||
"edit_game_modal_icon_preview": "Icon preview",
|
||||
"edit_game_modal_logo": "Logo",
|
||||
"edit_game_modal_select_logo": "Select logo",
|
||||
"edit_game_modal_logo_preview": "Logo preview",
|
||||
"edit_game_modal_hero": "Library Hero",
|
||||
"edit_game_modal_select_hero": "Select library hero image",
|
||||
"edit_game_modal_hero_preview": "Library hero image preview",
|
||||
"edit_game_modal_cancel": "Cancel",
|
||||
"edit_game_modal_update": "Update",
|
||||
"edit_game_modal_updating": "Updating...",
|
||||
"edit_game_modal_fill_required": "Please fill in all required fields",
|
||||
"edit_game_modal_success": "Assets updated successfully",
|
||||
"edit_game_modal_failed": "Failed to update assets",
|
||||
"edit_game_modal_image_filter": "Image",
|
||||
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
|
||||
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
|
||||
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
|
||||
"edit_game_modal_assets": "Assets",
|
||||
"edit_game_modal_drop_icon_image_here": "Drop icon image here",
|
||||
"edit_game_modal_drop_logo_image_here": "Drop logo image here",
|
||||
"edit_game_modal_drop_hero_image_here": "Drop hero image here",
|
||||
"edit_game_modal_drop_to_replace_icon": "Drop to replace icon",
|
||||
"edit_game_modal_drop_to_replace_logo": "Drop to replace logo",
|
||||
"edit_game_modal_drop_to_replace_hero": "Drop to replace hero"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
@@ -88,6 +137,7 @@
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"accuracy": "{{accuracy}}% accuracy",
|
||||
"add_to_library": "Add to library",
|
||||
"already_in_library": "Already in library",
|
||||
"remove_from_library": "Remove from library",
|
||||
"no_downloads": "No downloads available",
|
||||
"play_time": "Played for {{amount}}",
|
||||
@@ -196,15 +246,53 @@
|
||||
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||
"download_error_not_cached_on_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||
"update_playtime_title": "Update playtime",
|
||||
"update_playtime_description": "Manually update the playtime for {{game}}",
|
||||
"update_playtime": "Update playtime",
|
||||
"update_playtime_success": "Playtime updated successfully",
|
||||
"update_playtime_error": "Failed to update playtime",
|
||||
"update_game_playtime": "Update game playtime",
|
||||
"manual_playtime_warning": "Your hours will be marked as manually updated, and this cannot be undone.",
|
||||
"manual_playtime_tooltip": "This playtime has been manually updated",
|
||||
"download_error_not_cached_on_torbox": "This download is not available on TorBox and polling download status from TorBox is not yet available.",
|
||||
"download_error_not_cached_on_hydra": "This download is not available on Nimbus.",
|
||||
"game_removed_from_favorites": "Game removed from favorites",
|
||||
"game_added_to_favorites": "Game added to favorites",
|
||||
"game_removed_from_pinned": "Game removed from pinned",
|
||||
"game_added_to_pinned": "Game added to pinned",
|
||||
"automatically_extract_downloaded_files": "Automatically extract downloaded files",
|
||||
"create_start_menu_shortcut": "Create Start Menu shortcut",
|
||||
"invalid_wine_prefix_path": "Invalid Wine prefix path",
|
||||
"invalid_wine_prefix_path_description": "The path to the Wine prefix is invalid. Please check the path and try again.",
|
||||
"missing_wine_prefix": "Wine prefix is required to create a backup on Linux"
|
||||
"missing_wine_prefix": "Wine prefix is required to create a backup on Linux",
|
||||
"artifact_renamed": "Backup renamed successfully",
|
||||
"rename_artifact": "Rename Backup",
|
||||
"rename_artifact_description": "Rename the backup to a more descriptive name",
|
||||
"artifact_name_label": "Backup name",
|
||||
"artifact_name_placeholder": "Enter a name for the backup",
|
||||
"save_changes": "Save changes",
|
||||
"required_field": "This field is required",
|
||||
"max_length_field": "This field must be less than {{length}} characters",
|
||||
"freeze_backup": "Pin it so it's not overwritten by automatic backups",
|
||||
"unfreeze_backup": "Unpin it",
|
||||
"backup_frozen": "Backup pinned",
|
||||
"backup_unfrozen": "Backup unpinned",
|
||||
"backup_freeze_failed": "Failed to freeze backup",
|
||||
"backup_freeze_failed_description": "You must leave at least one free slot for automatic backups",
|
||||
"edit_game_modal_button": "Customize game assets",
|
||||
"game_details": "Game Details",
|
||||
"currency_symbol": "$",
|
||||
"currency_country": "us",
|
||||
"prices": "Prices",
|
||||
"no_prices_found": "No prices found",
|
||||
"view_all_prices": "Click to view all prices",
|
||||
"retail_price": "Retail price",
|
||||
"keyshop_price": "Keyshop price",
|
||||
"historical_retail": "Historical retail",
|
||||
"historical_keyshop": "Historical keyshop",
|
||||
"language": "Language",
|
||||
"caption": "Caption",
|
||||
"audio": "Audio"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@@ -364,6 +452,7 @@
|
||||
"installing_common_redist": "Installing…",
|
||||
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
|
||||
"extract_files_by_default": "Extract files by default after download",
|
||||
"enable_steam_achievements": "Enable search for Steam achievements",
|
||||
"achievement_custom_notification_position": "Achievement custom notification position",
|
||||
"top-left": "Top left",
|
||||
"top-center": "Top center",
|
||||
@@ -423,10 +512,17 @@
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} hours",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"amount_hours_short": "{{amount}}h",
|
||||
"amount_minutes_short": "{{amount}}m",
|
||||
"last_time_played": "Last played {{period}}",
|
||||
"activity": "Recent Activity",
|
||||
"library": "Library",
|
||||
"pinned": "Pinned",
|
||||
"achievements_earned": "Achievements earned",
|
||||
"played_recently": "Played recently",
|
||||
"playtime": "Playtime",
|
||||
"total_play_time": "Total playtime",
|
||||
"manual_playtime_tooltip": "This playtime has been manually updated",
|
||||
"no_recent_activity_title": "Hmmm… nothing here",
|
||||
"no_recent_activity_description": "You haven't played any games recently. It's time to change that!",
|
||||
"display_name": "Display name",
|
||||
@@ -499,7 +595,11 @@
|
||||
"achievements_unlocked": "Achievements Unlocked",
|
||||
"earned_points": "Earned points",
|
||||
"show_achievements_on_profile": "Show your achievements on your profile",
|
||||
"show_points_on_profile": "Show your earned points on your profile"
|
||||
"show_points_on_profile": "Show your earned points on your profile",
|
||||
"error_adding_friend": "Could not send friend request. Please check friend code",
|
||||
"friend_code_length_error": "Friend code must have 8 characters",
|
||||
"game_removed_from_pinned": "Game removed from pinned",
|
||||
"game_added_to_pinned": "Game added to pinned"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Achievement unlocked",
|
||||
|
||||
@@ -1,33 +1,76 @@
|
||||
{
|
||||
"language_name": "Español",
|
||||
"app": {
|
||||
"successfully_signed_in": "Sesión iniciada exitosamente"
|
||||
"successfully_signed_in": "Iniciaste sesión exitosamente"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destacado",
|
||||
"surprise_me": "¡Sorpréndeme!",
|
||||
"no_results": "Sin resultados encontrados",
|
||||
"start_typing": "Empieza a escribir para buscar...",
|
||||
"hot": "Popular Ahora",
|
||||
"no_results": "No se encontraron resultados",
|
||||
"start_typing": "Empezá a escribir para buscar...",
|
||||
"hot": "Tendencias",
|
||||
"weekly": "📅 Mejores juegos de la semana",
|
||||
"achievements": "🏆 Juegos para completar"
|
||||
"achievements": "🏆 Juegos para platinar"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Descargas",
|
||||
"settings": "Ajustes",
|
||||
"my_library": "Mi biblioteca",
|
||||
"my_library": "Mi Librería",
|
||||
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
||||
"filter": "Buscar en la biblioteca",
|
||||
"filter": "Filtrar Librería",
|
||||
"home": "Inicio",
|
||||
"queued": "{{title}} (En cola)",
|
||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"sign_in": "Iniciar Sesión",
|
||||
"friends": "Amigos",
|
||||
"need_help": "¿Necesitas ayuda?",
|
||||
"favorites": "Favoritos"
|
||||
"need_help": "¿Necesitás ayuda?",
|
||||
"favorites": "Favoritos",
|
||||
"playable_button_title": "Solo mostrar juegos que podés jugar en este momento",
|
||||
"add_custom_game_tooltip": "Añadir juego personalizado",
|
||||
"show_playable_only_tooltip": "Mostrar Solo Jugable",
|
||||
"custom_game_modal": "Añadir juego personalizado",
|
||||
"custom_game_modal_description": "Añadí un juego personalizado a tu librería seleccionando el ejecutable",
|
||||
"custom_game_modal_executable_path": "Ruta del Ejecutable",
|
||||
"custom_game_modal_select_executable": "Seleccionar archivo ejecutable",
|
||||
"custom_game_modal_title": "Título",
|
||||
"custom_game_modal_enter_title": "Ingresá el título",
|
||||
"custom_game_modal_browse": "Buscar",
|
||||
"custom_game_modal_cancel": "Cancelar",
|
||||
"custom_game_modal_add": "Añadir juego",
|
||||
"custom_game_modal_adding": "Añadiendo juego...",
|
||||
"custom_game_modal_success": "Juego personalizado añadido exitosamente",
|
||||
"custom_game_modal_failed": "Error al añadir juego personalizado",
|
||||
"custom_game_modal_executable": "Ejecutable",
|
||||
"edit_game_modal": "Personalizar recursos",
|
||||
"edit_game_modal_description": "Personaliza los recursos y detalles del juego",
|
||||
"edit_game_modal_title": "Título",
|
||||
"edit_game_modal_enter_title": "Ingresá el título",
|
||||
"edit_game_modal_image": "Imagen",
|
||||
"edit_game_modal_select_image": "Seleccionar imagen",
|
||||
"edit_game_modal_browse": "Navegar",
|
||||
"edit_game_modal_image_preview": "Vista previa de imagen",
|
||||
"edit_game_modal_icon": "Ícono",
|
||||
"edit_game_modal_select_icon": "Seleccionar ícono",
|
||||
"edit_game_modal_icon_preview": "Vista previa de ícono",
|
||||
"edit_game_modal_logo": "Logo",
|
||||
"edit_game_modal_select_logo": "Seleccionar logo",
|
||||
"edit_game_modal_logo_preview": "Vista previa del logo",
|
||||
"edit_game_modal_hero": "Library Hero",
|
||||
"edit_game_modal_select_hero": "Seleccionar una imagen de Library Hero",
|
||||
"edit_game_modal_hero_preview": "Vista previa de library hero",
|
||||
"edit_game_modal_cancel": "Cancelar",
|
||||
"edit_game_modal_update": "Actualizar",
|
||||
"edit_game_modal_updating": "Actualizando...",
|
||||
"edit_game_modal_fill_required": "Por favor rellená todos los espacios requeridos",
|
||||
"edit_game_modal_success": "Recursos actualizados exitosamente",
|
||||
"edit_game_modal_failed": "Error al actualizar los recursos",
|
||||
"edit_game_modal_image_filter": "Imagen",
|
||||
"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",
|
||||
"edit_game_modal_assets": "Recursos"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar juegos",
|
||||
@@ -36,348 +79,409 @@
|
||||
"downloads": "Descargas",
|
||||
"search_results": "Resultados de búsqueda",
|
||||
"settings": "Ajustes",
|
||||
"version_available_install": "Versión {{version}} disponible. Presiona acá para descargar y reinstalar.",
|
||||
"version_available_download": "Versión {{version}} disponible. Presiona aquí para descargar."
|
||||
"version_available_install": "Versión {{version}} disponible. Presiona acá para reiniciar e instalar.",
|
||||
"version_available_download": "Versión {{version}} disponible. Presiona acá para descargar."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Sin descargas en progreso",
|
||||
"downloading_metadata": "Descargando metadatos de {{title}}…",
|
||||
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Calculando tiempo restante…",
|
||||
"installation_complete": "Instalación completada",
|
||||
"installation_complete_message": "Common redistributables instalados exitosamente",
|
||||
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Restante {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Comprobando tiempo restante…",
|
||||
"checking_files": "Revisando archivos de {{title}}… ({{percentage}} completado)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)"
|
||||
"installation_complete": "Instalación completada",
|
||||
"installation_complete_message": "Common redistributables instalados correctamente"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filtrar…",
|
||||
"developers": "Desarrolladores",
|
||||
"genres": "Géneros",
|
||||
"tags": "Marcadores",
|
||||
"tags": "Etiquetas",
|
||||
"publishers": "Editores",
|
||||
"download_sources": "Fuentes de descarga",
|
||||
"download_sources": "Descargando fuentes",
|
||||
"result_count": "{{resultCount}} resultados",
|
||||
"filter_count": "{{filterCount}} disponibles",
|
||||
"filter_count": "{{filterCount}} disponible",
|
||||
"clear_filters": "Limpiar {{filterCount}} seleccionados"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Ver opciones de descargas",
|
||||
"automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados",
|
||||
"download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.",
|
||||
"download_options_zero": "No hay opciones de descargas disponibles",
|
||||
"open_download_options": "Abrir opciones de descargas",
|
||||
"download_options_zero": "Sin opciones de descargas",
|
||||
"download_options_one": "{{count}} opción de descarga",
|
||||
"download_options_other": "{{count}} opciones de descargas",
|
||||
"updated_at": "Actualizado el: {{updated_at}}",
|
||||
"updated_at": "Actualizado el {{updated_at}}",
|
||||
"install": "Instalar",
|
||||
"resume": "Continuar",
|
||||
"pause": "Pausa",
|
||||
"resume": "Resumir",
|
||||
"pause": "Pausar",
|
||||
"cancel": "Cancelar",
|
||||
"remove": "Eliminar",
|
||||
"space_left_on_disk": "{{space}} restantes en el disco",
|
||||
"eta": "Tiempo restante: {{eta}}",
|
||||
"remove": "Remover",
|
||||
"space_left_on_disk": "{{space}} restante en el disco",
|
||||
"eta": "Conclusión {{eta}}",
|
||||
"calculating_eta": "Calculando tiempo restante…",
|
||||
"downloading_metadata": "Descargando metadatos…",
|
||||
"filter": "Buscar repacks",
|
||||
"filter": "Filtrar repacks",
|
||||
"requirements": "Requisitos del Sistema",
|
||||
"minimum": "Mínimos",
|
||||
"recommended": "Recomendados",
|
||||
"paused": "Pausado",
|
||||
"release_date": "Fecha de lanzamiento: {{date}}",
|
||||
"publisher": "Publicado por: {{publisher}}",
|
||||
"paused": "En Pausa",
|
||||
"release_date": "Lanzado el {{date}}",
|
||||
"publisher": "Públicado por {{publisher}}",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos",
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"accuracy": "{{accuracy}}% precisión",
|
||||
"add_to_library": "Agregar a la biblioteca",
|
||||
"remove_from_library": "Eliminar de la biblioteca",
|
||||
"no_downloads": "No hay descargas disponibles",
|
||||
"play_time": "Has jugado {{amount}}",
|
||||
"last_time_played": "Jugado por última vez: {{period}}",
|
||||
"not_played_yet": "Aún no has jugado a {{title}}",
|
||||
"accuracy": "{{accuracy}}% completista",
|
||||
"add_to_library": "Añadir a la librería",
|
||||
"already_in_library": "Ya está en la librería",
|
||||
"remove_from_library": "Eliminar de la librería",
|
||||
"no_downloads": "Sin descargas disponibles",
|
||||
"play_time": "Jugado por {{amount}}",
|
||||
"last_time_played": "Última vez jugado {{period}}",
|
||||
"not_played_yet": "No has jugado a {{title}} todavía",
|
||||
"next_suggestion": "Siguiente sugerencia",
|
||||
"play": "Jugar",
|
||||
"deleting": "Eliminando instalador…",
|
||||
"close": "Cerrar",
|
||||
"playing_now": "Jugando ahora",
|
||||
"change": "Cambiar",
|
||||
"repacks_modal_description": "Selecciona el repack que quieres descargar",
|
||||
"select_folder_hint": "Para cambiar la carpeta predeterminada, ve a <0>Ajustes</0>",
|
||||
"repacks_modal_description": "Elegí el repack que querés descargar",
|
||||
"select_folder_hint": "Si querés cambiar la carpeta por defecto, andá a <0>Ajustes</0>",
|
||||
"download_now": "Descargar ahora",
|
||||
"no_shop_details": "No se pudieron obtener detalles de la tienda.",
|
||||
"download_options": "Opciones de descarga",
|
||||
"download_path": "Ruta de descarga",
|
||||
"previous_screenshot": "Anterior captura",
|
||||
"next_screenshot": "Siguiente captura",
|
||||
"screenshot": "Captura {{number}}",
|
||||
"open_screenshot": "Abrir captura {{number}}",
|
||||
"download_settings": "Ajustes de descarga",
|
||||
"downloader": "Método de descarga",
|
||||
"screenshot": "Captura número {{number}}",
|
||||
"open_screenshot": "Abrir captura número {{number}}",
|
||||
"download_settings": "Descargar ajustes",
|
||||
"downloader": "Descargador",
|
||||
"select_executable": "Seleccionar",
|
||||
"no_executable_selected": "No se seleccionó un ejecutable",
|
||||
"no_executable_selected": "Sin ejecutable seleccionado",
|
||||
"open_folder": "Abrir carpeta",
|
||||
"open_download_location": "Ver archivos descargados",
|
||||
"create_shortcut": "Crear acceso directo en el escritorio",
|
||||
"remove_files": "Eliminar archivos",
|
||||
"create_shortcut": "Crear atajo en el escritorio",
|
||||
"clear": "Limpiar",
|
||||
"remove_files": "Remover archivos",
|
||||
"remove_from_library_title": "¿Estás seguro?",
|
||||
"remove_from_library_description": "Esto eliminará {{game}} de tu biblioteca",
|
||||
"remove_from_library_description": "Esto va eliminará {{game}} de tu librería",
|
||||
"options": "Opciones",
|
||||
"executable_section_title": "Ejecutable",
|
||||
"executable_section_description": "Ruta del archivo que se ejecutará cuando se presione \"Jugar\"",
|
||||
"executable_section_description": "Ruta del archivo que se ejecutará cuando presiones \"Jugar\"",
|
||||
"downloads_section_title": "Descargas",
|
||||
"downloads_section_description": "Buscar actualizaciones u otras versiones de este juego",
|
||||
"danger_zone_section_title": "Opciones Avanzadas",
|
||||
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)",
|
||||
"downloads_section_description": "Revisar actualizaciones u otras versiones del juego",
|
||||
"danger_zone_section_title": "Zona de Peligro",
|
||||
"danger_zone_section_description": "Remover este juego de tu librería o los archivos descargados por Hydra",
|
||||
"download_in_progress": "Descarga en progreso",
|
||||
"download_paused": "Descarga pausada",
|
||||
"last_downloaded_option": "Última opción de descarga",
|
||||
"create_steam_shortcut": "Crear atajo de Steam",
|
||||
"last_downloaded_option": "Última opción descargada",
|
||||
"create_shortcut_success": "Atajo creado con éxito",
|
||||
"you_might_need_to_restart_steam": "Es posible que necesites reiniciar Steam para ver los cambios",
|
||||
"create_shortcut_error": "Error al crear un atajo",
|
||||
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
||||
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
|
||||
"you_might_need_to_restart_steam": "Probablemente necesités reiniciar Steam para ver cambios",
|
||||
"create_shortcut_error": "Error al crear atajo",
|
||||
"nsfw_content_title": "Este juego tiene contenido inapropiado",
|
||||
"nsfw_content_description": "{{title}} tiene contenido no apto para todas las edades. ¿Querés continuar igualmente?",
|
||||
"allow_nsfw_content": "Continuar",
|
||||
"refuse_nsfw_content": "No, gracias",
|
||||
"refuse_nsfw_content": "Regresar",
|
||||
"stats": "Estadísticas",
|
||||
"download_count": "Downloads",
|
||||
"player_count": "Jugadores activos",
|
||||
"download_error": "Esta opción de descarga no está disponible.",
|
||||
"download_count": "Descargas",
|
||||
"player_count": "Jugadores activos",
|
||||
"download_error": "Esta opción de descarga no está disponible",
|
||||
"download": "Descargar",
|
||||
"executable_path_in_use": "El ejecutable se encuentra en uso por \"{{game}}\"",
|
||||
"executable_path_in_use": "El ejecutable ya se está usando por \"{{game}}\"",
|
||||
"warning": "Advertencia:",
|
||||
"hydra_needs_to_remain_open": "Para esta descarga, Hydra necesita mantenerse abierta hasta que concluya. En caso de que Hydra se cierre antes de que concluya, podrías perder todo el progreso.",
|
||||
"hydra_needs_to_remain_open": "para esta descarga, Hydra necesita estar abierta hasta que termine. Si se cierra antes de completar, perderás todo el progreso.",
|
||||
"achievements": "Logros",
|
||||
"achievements_count": "Logros {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Guardado en la nube",
|
||||
"cloud_save_description": "Guarda tu progreso en la nube y continúa jugando en cualquier dispositivo",
|
||||
"backups": "Copias de Seguridad",
|
||||
"cloud_save_description": "Guardá tu progreso en la nube y jugá en cualquier dispositivo",
|
||||
"backups": "Copia de seguridad",
|
||||
"install_backup": "Instalar",
|
||||
"delete_backup": "Eliminar",
|
||||
"create_backup": "Nueva Copia de Seguridad",
|
||||
"last_backup_date": "Última copia de seguridad el {{date}}",
|
||||
"no_backup_preview": "No se encontraron datos de guardados para este juego",
|
||||
"create_backup": "Nueva copia de seguridad",
|
||||
"last_backup_date": "Última copia de seguridad {{date}}",
|
||||
"no_backup_preview": "No se han encotrado puntos de guardado para este juego",
|
||||
"restoring_backup": "Restaurando copia de seguridad ({{progress}} completado)…",
|
||||
"uploading_backup": "Subiendo copia de seguridad…",
|
||||
"no_backups": "No has creado ninguna copia de seguridad para este juego aún",
|
||||
"no_backups": "No has creado ninguna copia de seguridad para este juego todavía",
|
||||
"backup_uploaded": "Copia de seguridad subida",
|
||||
"backup_deleted": "Copia de seguridad eliminada",
|
||||
"backup_restored": "Copia de seguridad restaurada",
|
||||
"see_all_achievements": "Ver todos los logros",
|
||||
"sign_in_to_see_achievements": "Inicia sesión para ver los logros",
|
||||
"sign_in_to_see_achievements": "Iniciá sesión para ver los logros",
|
||||
"mapping_method_automatic": "Automático",
|
||||
"mapping_method_manual": "Manual",
|
||||
"mapping_method_label": "Método de mapeo",
|
||||
"files_automatically_mapped": "Archivos mapeados automáticamente",
|
||||
"mapping_method_label": "Método de mapeado",
|
||||
"files_automatically_mapped": "Archivos automáticamente mapeados",
|
||||
"no_backups_created": "Sin copias de seguridad creadas para este juego",
|
||||
"manage_files": "Gestionar archivos",
|
||||
"loading_save_preview": "Buscando datos de guardados de juegos…",
|
||||
"manage_files": "Administrar archivos",
|
||||
"loading_save_preview": "Buscando por guardado de juegos…",
|
||||
"wine_prefix": "Prefijo de Wine",
|
||||
"wine_prefix_description": "El prefijo de Wine usado para ejecutar este juego",
|
||||
"wine_prefix_description": "El prefijo de Wine usado para este juego",
|
||||
"launch_options": "Opciones para iniciar",
|
||||
"launch_options_description": "Los usuarios avanzados pueden ingresar sus modificaciones para el inicio de sus juegos (característica experimental)",
|
||||
"launch_options_placeholder": "Sin parámetro específicado",
|
||||
"no_download_option_info": "Sin información disponible",
|
||||
"backup_deletion_failed": "La eliminación de la copia de seguridad falló",
|
||||
"max_number_of_artifacts_reached": "Número máximo de copias de seguridad de este juego alcanzadas",
|
||||
"achievements_not_sync": "Tus logros no están sincronizados",
|
||||
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
||||
"backup_deletion_failed": "Error al eliminar copia de seguridad",
|
||||
"max_number_of_artifacts_reached": "Máximo de copias de seguridad alcanzadas para este juego",
|
||||
"achievements_not_sync": "Revisá como sincronizar tus logros'",
|
||||
"manage_files_description": "Elegí que archivos se guardarán y restaurarán de la copia de seguridad",
|
||||
"select_folder": "Seleccionar carpeta",
|
||||
"backup_from": "Copia de seguridad de {{date}}",
|
||||
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
|
||||
"enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube",
|
||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||
"clear": "Limpiar",
|
||||
"no_directory_selected": "No se seleccionó un directorio",
|
||||
"launch_options": "Opciones de Inicio",
|
||||
"launch_options_description": "Los usuarios avanzados pueden introducir sus propias modificaciones de opciones de inicio (característica experimental)",
|
||||
"launch_options_placeholder": "Sin parámetro específicado",
|
||||
"no_write_permission": "No se puede descargar en este directorio. Presiona aquí para aprender más.",
|
||||
"enable_automatic_cloud_sync": "Habilitar sincronización con la nube",
|
||||
"custom_backup_location_set": "Ubicación de copia de seguridad personalizada",
|
||||
"no_directory_selected": "Sin directorio seleccionado",
|
||||
"no_write_permission": "No se puede descargar en este directorio. Presioná acá para más información.",
|
||||
"reset_achievements": "Reiniciar logros",
|
||||
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||
"reset_achievements_title": "¿Estás seguro?",
|
||||
"reset_achievements_success": "Logros reiniciados exitosamente",
|
||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros",
|
||||
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
|
||||
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
|
||||
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
||||
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y el estado de descarga del sondeo aún no está disponible.",
|
||||
"reset_achievements_description": "Esto va a reiniciar todos los logros para {{game}}",
|
||||
"reset_achievements_title": "¿Querés continuar?",
|
||||
"reset_achievements_success": "Logros reiniciados éxitosamente",
|
||||
"reset_achievements_error": "Error al reiniciar logros",
|
||||
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de GoFile. Esperá a que se reinice.",
|
||||
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nuevas descargas. Revisá los ajustes de tu cuenta y probá de nuevo.",
|
||||
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y no está disponible el estado de descarga de sondeo todavía.",
|
||||
"update_playtime_title": "Actualizar tiempo de juego",
|
||||
"update_playtime_description": "Actualizar manualmente el tiempo de juego para {{game}}",
|
||||
"update_playtime": "Actualizar tiempo de juego",
|
||||
"update_playtime_success": "Tiempo de juego actualizado éxitosamente",
|
||||
"update_playtime_error": "Error al actualizar el tiempo de juego",
|
||||
"update_game_playtime": "Actualizar tu tiempo de juego",
|
||||
"manual_playtime_warning": "Tus horas de juego se marcarán como actualizadas manualmente, y esto no se puede deshacer.",
|
||||
"manual_playtime_tooltip": "Este tiempo de juego se ha actualizad manualmente",
|
||||
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y no está disponible el estado de descarga de sondeo todavía.",
|
||||
"download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.",
|
||||
"game_removed_from_favorites": "Juego eliminado de favoritos",
|
||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||
"game_removed_from_favorites": "Juego removido de favoritos",
|
||||
"invalid_wine_prefix_path": "Ruta de prefixo Wine inválida",
|
||||
"invalid_wine_prefix_path_description": "La ruta al prefixo Wine es inválida. Por favor, verifica la ruta y vuelve a intentarlo.",
|
||||
"missing_wine_prefix": ""
|
||||
"game_removed_from_pinned": "Juego removido de fijados",
|
||||
"game_added_to_pinned": "Juego añadido a fijados",
|
||||
"automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados",
|
||||
"create_start_menu_shortcut": "Crear un atajo en el Menú de Inicio",
|
||||
"invalid_wine_prefix_path": "Ruta inválida del prefijo de Wine",
|
||||
"invalid_wine_prefix_path_description": "La ruta al prefijo de Wine es inválida. Por favor revisá la ruta y probá de nuevo.",
|
||||
"missing_wine_prefix": "EL prefijo de Wine es requerido para hacer una copia en Linux",
|
||||
"artifact_renamed": "Copia de seguridad renombrada éxitosamente",
|
||||
"rename_artifact": "Renombrar copia de seguridad",
|
||||
"rename_artifact_description": "Renombrar copia de seguridad con un nombre más descriptivo",
|
||||
"artifact_name_label": "Nombre de la copia de seguridad",
|
||||
"artifact_name_placeholder": "Introducí un nombre para la copia de seguridad",
|
||||
"save_changes": "Guardar cambios",
|
||||
"required_field": "Este campo es requerido",
|
||||
"max_length_field": "Este campo debe tener menos de {{length}} carácteres",
|
||||
"freeze_backup": "Fíjalo así no se re-escríbira por copias de seguridad automáticas",
|
||||
"unfreeze_backup": "Dejar de fijar",
|
||||
"backup_frozen": "Copia de seguridad fijada",
|
||||
"backup_unfrozen": "Copia de seguridad desfijada",
|
||||
"backup_freeze_failed": "Error al congelar tu copia de seguridad",
|
||||
"backup_freeze_failed_description": "Tenés que tener mínimo un espacio para copias de seguridad automáticas",
|
||||
"edit_game_modal_button": "Personalizar recursos de juego",
|
||||
"game_details": "Detalles del juego",
|
||||
"currency_symbol": "$",
|
||||
"currency_country": "us",
|
||||
"prices": "Precios",
|
||||
"no_prices_found": "No se encontraron precios",
|
||||
"view_all_prices": "Presioná acá para ver todos los precios",
|
||||
"retail_price": "Precio recomendado",
|
||||
"keyshop_price": "Precio de tiendas de terceros",
|
||||
"historical_retail": "Precio de tiendas",
|
||||
"historical_keyshop": "Precio de tiendas de terceros",
|
||||
"language": "Idioma",
|
||||
"caption": "Subtítulo",
|
||||
"audio": "Audio"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activar Hydra",
|
||||
"installation_id": "ID de la Instalación:",
|
||||
"enter_activation_code": "Introduce tu código de activación",
|
||||
"message": "Si no sabes donde obtener el código, no deberías de tener esto.",
|
||||
"installation_id": "ID de Instalación:",
|
||||
"enter_activation_code": "Introducí tu código de activación",
|
||||
"message": "Si no sabes donde preguntar por esto, entonces no tenés que tener esto.",
|
||||
"activate": "Activar",
|
||||
"loading": "Cargando…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Resumir",
|
||||
"pause": "Pausa",
|
||||
"eta": "Finalizando en {{eta}}",
|
||||
"paused": "En Pausa",
|
||||
"pause": "Pausar",
|
||||
"eta": "Tiempo de finalizción {{eta}}",
|
||||
"paused": "Pausado",
|
||||
"verifying": "Verificando…",
|
||||
"completed": "Completado",
|
||||
"removed": "No descargado",
|
||||
"cancel": "Cancelar",
|
||||
"filter": "Buscar juegos descargados",
|
||||
"remove": "Eliminar",
|
||||
"filter": "Filtrar juegos descargados",
|
||||
"remove": "Remover",
|
||||
"downloading_metadata": "Descargando metadatos…",
|
||||
"deleting": "Eliminando instalador…",
|
||||
"delete": "Eliminar instalador",
|
||||
"delete_modal_title": "¿Estás seguro?",
|
||||
"delete_modal_description": "Esto eliminará todos los archivos de la instalación del repack del juego de tu computadora. (Si ya instalaste el juego, puedes eliminar esto, no afectará al juego)",
|
||||
"deleting": "Eliminado instalador…",
|
||||
"delete": "Remover instalador",
|
||||
"delete_modal_title": "¿Querés continuar?",
|
||||
"delete_modal_description": "Esto eliminará todos los archivos del instalador de tu computadora",
|
||||
"install": "Instalar",
|
||||
"download_in_progress": "En progreso",
|
||||
"queued_downloads": "Descargas en cola",
|
||||
"downloads_completed": "Completado",
|
||||
"queued": "En cola",
|
||||
"no_downloads_title": "Esto está tan... vacío",
|
||||
"no_downloads_description": "No has descargado nada con Hydra... aún, ¡pero nunca es tarde para comenzar!.",
|
||||
"checking_files": "Verificando archivos…",
|
||||
"seeding": "Seeding",
|
||||
"stop_seeding": "Detener seeding",
|
||||
"resume_seeding": "Continuar seeding",
|
||||
"no_downloads_title": "Esto está... tan, ¿vacío?",
|
||||
"no_downloads_description": "No has descargado nada con Hydra, pero nunca es tarde para comenzar.",
|
||||
"checking_files": "Revisando archivos…",
|
||||
"seeding": "Sembrando",
|
||||
"stop_seeding": "Dejar de sembrar",
|
||||
"resume_seeding": "Continuar sembrando",
|
||||
"options": "Administrar",
|
||||
"extract": "Extraer archivos",
|
||||
"extracting": "Extrayendo archivos…",
|
||||
"options": "Gestionar"
|
||||
"extracting": "Extrayendo archivos…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Ruta de descarga",
|
||||
"common_redist": "Common redistributables",
|
||||
"common_redist_description": "Las Common redistributables son requeridos para ejecutar algunos juegos. Es recomendado instalarlos para evitar problemas.",
|
||||
"create_real_debrid_account": "Presiona acá si no tienes una cuenta de Real-Debrid aún",
|
||||
"create_torbox_account": "Presiona acá si no tienes una cuenta de TorBox aún",
|
||||
"install_common_redist": "Instalar",
|
||||
"installing_common_redist": "Instalando…",
|
||||
"show_download_speed_in_megabytes": "Mostrar velocidad de descargar en megabytes por segundo",
|
||||
"change": "Cambiar",
|
||||
"change": "Actualizar",
|
||||
"notifications": "Notificaciones",
|
||||
"enable_download_notifications": "Cuando se completa una descarga",
|
||||
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
||||
"real_debrid_api_token_label": "Token API de Real-Debrid",
|
||||
"quit_app_instead_hiding": "Salir de Hydra en vez de minimizar en la bandeja del sistema",
|
||||
"launch_with_system": "Iniciar Hydra al inicio del sistema",
|
||||
"enable_download_notifications": "Cuando una descarga se completa",
|
||||
"enable_repack_list_notifications": "Cuando un nuevo repack se añade",
|
||||
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||
"quit_app_instead_hiding": "No ocultar Hydra cuando se cierra",
|
||||
"launch_with_system": "Iniciar Hydra con el sistema",
|
||||
"general": "General",
|
||||
"behavior": "Otros",
|
||||
"download_sources": "Fuentes de descarga",
|
||||
"behavior": "Comportamiento",
|
||||
"download_sources": "Fuentes de descargas",
|
||||
"language": "Idioma",
|
||||
"api_token": "Token API",
|
||||
"enable_real_debrid": "Activar Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
||||
"debrid_invalid_token": "Token de API inválido",
|
||||
"debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
||||
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
||||
"api_token": "API Token",
|
||||
"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>",
|
||||
"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",
|
||||
"changes_saved": "Ajustes guardados exitosamente",
|
||||
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
||||
"changes_saved": "Cambios guardados éxitosamente",
|
||||
"download_sources_description": "Hydra va a recoger los links de descarga de cada fuente. La URL de origen debe ser un enlace .json que contenga los enlaces de descarga.",
|
||||
"validate_download_source": "Validar",
|
||||
"remove_download_source": "Eliminar",
|
||||
"add_download_source": "Añadir fuente de descarga",
|
||||
"download_count_zero": "No hay descargas en la lista",
|
||||
"download_count_one": "{{countFormatted}} descarga en la lista",
|
||||
"download_count_other": "{{countFormatted}} descargas en la lista",
|
||||
"download_source_url": "Descargar URL de origen",
|
||||
"add_download_source_description": "Introduce la URL con el archivo .json",
|
||||
"download_source_up_to_date": "Al día",
|
||||
"remove_download_source": "Remover",
|
||||
"add_download_source": "Añadir fuente",
|
||||
"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",
|
||||
"add_download_source_description": "Introducí la URL del archivo .json",
|
||||
"download_source_up_to_date": "Actualizado",
|
||||
"download_source_errored": "Error",
|
||||
"sync_download_sources": "Sincronizar fuentes",
|
||||
"removed_download_source": "Fuente de descarga eliminada",
|
||||
"removed_download_sources": "Fuente de descarga eliminadas",
|
||||
"cancel_button_confirmation_delete_all_sources": "No",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sí, eliminar todo",
|
||||
"description_confirmation_delete_all_sources": "Eliminarás todas las fuentes de descarga",
|
||||
"confirm_button_confirmation_delete_all_sources": "Si, eliminar todo",
|
||||
"title_confirmation_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
||||
"removed_download_sources": "Fuentes de descarga eliminadas",
|
||||
"button_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
||||
"added_download_source": "Fuente de descarga añadida",
|
||||
"download_sources_synced": "Todas las fuentes de descargas están actualizadas.",
|
||||
"insert_valid_json_url": "Introduce una URL JSON válida",
|
||||
"found_download_option_zero": "No se encontró una opción de descarga",
|
||||
"found_download_option_one": "Se encontró {{countFormatted}} opción de descarga",
|
||||
"found_download_option_other": "Se encontraron {{countFormatted}} opciones de descarga",
|
||||
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
|
||||
"button_delete_all_sources": "Eliminar todo",
|
||||
"added_download_source": "Añadir fuente de descarga",
|
||||
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
|
||||
"insert_valid_json_url": "Introducí una URL de json válida",
|
||||
"found_download_option_zero": "Sin opciones de descargas encontrada",
|
||||
"found_download_option_one": "Encontrada {{countFormatted}} fuente de descarga",
|
||||
"found_download_option_other": "Encontradas {{countFormatted}} opciones de descargas",
|
||||
"import": "Importar",
|
||||
"public": "Público",
|
||||
"private": "Privado",
|
||||
"friends_only": "Solo amigos",
|
||||
"friends_only": "Sólo amigos",
|
||||
"privacy": "Privacidad",
|
||||
"profile_visibility": "Visibilidad del perfil",
|
||||
"profile_visibility_description": "Elige quién puede ver tu perfil y biblioteca",
|
||||
"required_field": "Este campo es obligatorio",
|
||||
"source_already_exists": "Esta fuente ya ha sido agregada.",
|
||||
"must_be_valid_url": "La fuente debe ser una URL válida.",
|
||||
"profile_visibility_description": "Elegí quién puede ver tú perfil y biblioteca",
|
||||
"required_field": "Este campo es requerido",
|
||||
"source_already_exists": "Esta fuente ya está añadida",
|
||||
"must_be_valid_url": "La fuente debe ser una URL válida",
|
||||
"blocked_users": "Usuarios bloqueados",
|
||||
"user_unblocked": "El usuario ha sido desbloqueado",
|
||||
"enable_achievement_notifications": "Cuando un logro se desbloquea",
|
||||
"user_unblocked": "Has desbloqueado a este usuario",
|
||||
"enable_achievement_notifications": "Cuando desbloqueás un logro",
|
||||
"launch_minimized": "Iniciar Hydra minimizado",
|
||||
"disable_nsfw_alert": "Desactivar alerta NSFW",
|
||||
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
|
||||
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos",
|
||||
"disable_nsfw_alert": "Deshabilitar alerta de NSFW",
|
||||
"seed_after_download_complete": "Sembrar después de completar una descarga",
|
||||
"show_hidden_achievement_description": "Mostrar logros ocultos antes de desbloquearlos",
|
||||
"account": "Cuenta",
|
||||
"account_data_updated_successfully": "Datos de la cuenta actualizados",
|
||||
"bill_sent_until": "Tú próxima factura se enviará el {{date}}",
|
||||
"current_email": "Correo actual:",
|
||||
"manage_subscription": "Gestionar suscripción",
|
||||
"no_email_account": "No has configurado un correo aún",
|
||||
"no_subscription": "Disfruta Hydra de la mejor manera",
|
||||
"no_users_blocked": "No tienes usuarios bloqueados",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
|
||||
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
|
||||
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||
"no_users_blocked": "No has bloqueado a ningún usuario",
|
||||
"subscription_active_until": "Tu Hydra Cloud está activo hasta {{date}}",
|
||||
"manage_subscription": "Administrar suscripción",
|
||||
"update_email": "Actualizar correo",
|
||||
"update_password": "Actualizar contraseña",
|
||||
"appearance": "Apariencia",
|
||||
"become_subscriber": "Sé Hydra Cloud",
|
||||
"cancel": "Cancelar",
|
||||
"clear_themes": "Limpiar",
|
||||
"create_theme": "Crear",
|
||||
"create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra",
|
||||
"create_theme_modal_title": "Crear tema personalizado",
|
||||
"delete_all_themes": "Eliminar todos los temas",
|
||||
"delete_all_themes_description": "Esto eliminará todos tus temas personalizados",
|
||||
"delete_theme": "Eliminar tema",
|
||||
"delete_theme_description": "Esto eliminará el tema {{theme}}",
|
||||
"edit_theme": "Editar tema",
|
||||
"update_password": "Cambiar contraseña",
|
||||
"current_email": "Correo actual:",
|
||||
"no_email_account": "No tenés ningún correo vinculado aún",
|
||||
"account_data_updated_successfully": "Datos de la cuenta actualizados correctamente",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_expired_at": "Tu suscripción expiró el {{date}}",
|
||||
"no_subscription": "Disfrutá Hydra de la mejor forma",
|
||||
"become_subscriber": "Sé parte de Hydra Cloud",
|
||||
"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.",
|
||||
"editor_tab_code": "Código",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Guardar",
|
||||
"enable_torbox": "Habilitar TorBox",
|
||||
"error_importing_theme": "Error al importar el tema",
|
||||
"web_store": "Tienda Web",
|
||||
"clear_themes": "Limpiar",
|
||||
"create_theme": "Crear",
|
||||
"create_theme_modal_title": "Crear tema personalizado",
|
||||
"create_theme_modal_description": "Crear un nuevo tema para personalizar el estilo de Hydra",
|
||||
"theme_name": "Nombre",
|
||||
"insert_theme_name": "Introducí un nombre del tema",
|
||||
"set_theme": "Usar tema",
|
||||
"unset_theme": "Dejar de usar tema",
|
||||
"delete_theme": "Eliminar tema",
|
||||
"edit_theme": "Editar tema",
|
||||
"delete_all_themes": "Eliminar todos los temas",
|
||||
"delete_all_themes_description": "Esto va a eliminar todos los temas personalizados",
|
||||
"delete_theme_description": "Esto va a eliminar el tema {{theme}}",
|
||||
"cancel": "Cancelar",
|
||||
"appearance": "Apariencia",
|
||||
"enable_torbox": "Activar TorBox",
|
||||
"torbox_description": "TorBox es un servicio premium de seedbox que incluso rivaliza los mejores servidores.",
|
||||
"torbox_account_linked": "Cuenta de TorBox vinculada",
|
||||
"create_real_debrid_account": "Presioná acá si todavía no tenés una cuenta de Real-Debrid",
|
||||
"create_torbox_account": "Presioná acá si todavía no tenés una cuenta de TorBox",
|
||||
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
|
||||
"name_min_length": "El nombre del tema debe tener mínimo 3 carácteres",
|
||||
"import_theme": "Importar tema",
|
||||
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
|
||||
"insert_theme_name": "Introducí el nombre del tema",
|
||||
"name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo",
|
||||
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.",
|
||||
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
|
||||
"set_theme": "Establecer tema",
|
||||
"theme_imported": "Tema importado exitosamente",
|
||||
"theme_name": "Nombre",
|
||||
"torbox_account_linked": "Cuenta de TorBox vinculada",
|
||||
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
|
||||
"unset_theme": "Desactivar tema",
|
||||
"web_store": "Tienda Web",
|
||||
"enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad",
|
||||
"enable_auto_install": "Descargar actualizaciones automáticamente"
|
||||
"error_importing_theme": "Error al importar el tema",
|
||||
"theme_imported": "Tema importado correctamente",
|
||||
"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.",
|
||||
"install_common_redist": "Instalar",
|
||||
"installing_common_redist": "Instalando…",
|
||||
"show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo",
|
||||
"extract_files_by_default": "Extraer archivos por defecto después de descargar",
|
||||
"enable_steam_achievements": "Habilitar búsqueda de logros de Steam",
|
||||
"achievement_custom_notification_position": "Posición de notificación de logros",
|
||||
"top-left": "Superior Izquierda",
|
||||
"top-center": "Superior Centro",
|
||||
"top-right": "Superior Derecha",
|
||||
"bottom-left": "Inferior Izquierda",
|
||||
"bottom-center": "Inferior Centro",
|
||||
"bottom-right": "Inferior Derecha",
|
||||
"enable_achievement_custom_notifications": "Habilitar notificación personalizada de logros",
|
||||
"alignment": "Centrado",
|
||||
"variation": "Variación",
|
||||
"default": "Defecto",
|
||||
"rare": "Raro",
|
||||
"platinum": "Platino",
|
||||
"hidden": "Oculto",
|
||||
"test_notification": "Probar notificación",
|
||||
"notification_preview": "Probar notificación de logro",
|
||||
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Descarga completada",
|
||||
"game_ready_to_install": "{{title}} está listo para instalar",
|
||||
"repack_list_updated": "Lista de repacks actualizadas",
|
||||
"repack_count_one": "{{count}} repack añadido",
|
||||
"repack_count_other": "{{count}} repacks añadidos",
|
||||
"new_update_available": "Versión {{version}} disponible",
|
||||
"restart_to_install_update": "Reiniciá Hydra para instalar la actualización",
|
||||
"notification_achievement_unlocked_title": "Logro desbloqueado para {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
||||
"new_friend_request_description": "{{displayName}} te envió una solicitud de amistad",
|
||||
"new_friend_request_title": "Nueva solicitud de amistad",
|
||||
"extraction_complete": "Extracción completada",
|
||||
"game_extracted": "{{title}} extraído exitosamente",
|
||||
"game_ready_to_install": "{{title}} está listo para instalarse",
|
||||
"repack_list_updated": "Lista de repacks actualizadas",
|
||||
"repack_count_one": "{{count}} repack ha sido añadido",
|
||||
"repack_count_other": "{{count}} repacks añadidos",
|
||||
"new_update_available": "Version {{version}} disponible",
|
||||
"restart_to_install_update": "Reinicia Hydra para instalar la actualización",
|
||||
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
||||
"new_friend_request_title": "Nueva solicitud de amistad",
|
||||
"new_friend_request_description": "{{displayName}} te envió una solicitud de amistad",
|
||||
"friend_started_playing_game": "{{displayName}} está jugando"
|
||||
"friend_started_playing_game": "{{displayName}} empezó a jugar un juego",
|
||||
"test_achievement_notification_title": "Esto es una notificación de prueba",
|
||||
"test_achievement_notification_description": "Piola, ¿verdad?"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
@@ -386,15 +490,15 @@
|
||||
"game_card": {
|
||||
"available_one": "Disponible",
|
||||
"available_other": "Disponibles",
|
||||
"no_downloads": "No hay descargas disponibles"
|
||||
"no_downloads": "Sin descargas disponibles"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programas no instalados",
|
||||
"description": "Los ejecutables de Wine o Lutris no se encontraron en tu sistema",
|
||||
"instructions": "Comprueba como instalar de forma correcta uno de los dos en tu distro de Linux para ejecutar el juego con normalidad"
|
||||
"description": "Ejecutables de Wine o Lutris executables no encontrados en tu sistema",
|
||||
"instructions": "Comprobá la forma correcta de instalar cualquiera de ellos en tu distribución de Linux para que el juego pueda ejecutarse con normalidad"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Botón de cierre"
|
||||
"close": "Botón de cerrar"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Cambiar visibilidad de contraseña"
|
||||
@@ -402,111 +506,120 @@
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"last_time_played": "Última vez jugado: {{period}}",
|
||||
"last_time_played": "Jugado por última vez el {{period}}",
|
||||
"activity": "Actividad reciente",
|
||||
"library": "Biblioteca",
|
||||
"total_play_time": "Has jugado",
|
||||
"no_recent_activity_title": "Que raro, no hay nada por acá...",
|
||||
"no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!",
|
||||
"display_name": "Nombre en pantalla",
|
||||
"library": "Librería",
|
||||
"pinned": "Fijado",
|
||||
"total_play_time": "Total de tiempo de juego",
|
||||
"achievements_earned": "Logros conseguidos",
|
||||
"played_recently": "Jugado recientemente",
|
||||
"playtime": "Tiempo de juego",
|
||||
"manual_playtime_tooltip": "Este tiempo de juego ha sido modificado manualmente",
|
||||
"no_recent_activity_title": "Hmmm… nada por acá",
|
||||
"no_recent_activity_description": "No has jugado nada recientemente. ¡Te toca cambiar eso!",
|
||||
"display_name": "Nombre a mostar",
|
||||
"saving": "Guardando",
|
||||
"save": "Guardar",
|
||||
"save": "Guardado",
|
||||
"edit_profile": "Editar perfil",
|
||||
"saved_successfully": "Guardado exitosamente",
|
||||
"try_again": "Por favor, intenta de nuevo",
|
||||
"sign_out_modal_title": "¿Estás seguro?",
|
||||
"try_again": "Por favor, intentá de nuevo",
|
||||
"sign_out_modal_title": "¿Querés continuar?",
|
||||
"cancel": "Cancelar",
|
||||
"successfully_signed_out": "Sesión cerrada exitosamente",
|
||||
"successfully_signed_out": "Cerraste sesión exitosamente",
|
||||
"sign_out": "Cerrar sesión",
|
||||
"playing_for": "Llevas jugando {{amount}}",
|
||||
"sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?",
|
||||
"add_friends": "Añadir amigos",
|
||||
"playing_for": "Jugando por {{amount}}",
|
||||
"sign_out_modal_text": "Tu librería está vinculada con esta cuenta. Cuando cerrés sesión, tu librería ya no será visible, y cualquier progreso no se guardará. ¿Querés continuar con el cierre de sesión?",
|
||||
"add_friends": "Añadir amistades",
|
||||
"add": "Añadir",
|
||||
"friend_code": "Código de amigo",
|
||||
"friend_code": "Código de amistad",
|
||||
"see_profile": "Ver perfil",
|
||||
"sending": "Enviando",
|
||||
"friend_request_sent": "Solicitud de amistad enviada",
|
||||
"friends": "Amigos",
|
||||
"friends_list": "Lista de amigos",
|
||||
"friends": "Amistades",
|
||||
"friends_list": "Lista de amistades",
|
||||
"user_not_found": "Usuario no encontrado",
|
||||
"block_user": "Bloquear usuario",
|
||||
"add_friend": "Añadir amigo",
|
||||
"add_friend": "Añadir amistad",
|
||||
"request_sent": "Solicitud enviada",
|
||||
"request_received": "Solicitud recibida",
|
||||
"accept_request": "Aceptar solicitud",
|
||||
"ignore_request": "Ignorar solicitud",
|
||||
"cancel_request": "Cancelar solicitud",
|
||||
"undo_friendship": "Eliminar amistad",
|
||||
"undo_friendship": "Deshacer amistad",
|
||||
"request_accepted": "Solicitud aceptada",
|
||||
"user_blocked_successfully": "Usuario bloqueado exitosamente",
|
||||
"user_block_modal_text": "Esto va a bloquear a {{displayName}}",
|
||||
"blocked_users": "Usuarios bloqueados",
|
||||
"unblock": "Desbloquear",
|
||||
"no_friends_added": "Todavía no tienes amigos añadidos",
|
||||
"no_friends_added": "No tenés amistades añadidas",
|
||||
"pending": "Pendiente",
|
||||
"no_pending_invites": "No tienes invitaciones pendientes",
|
||||
"no_blocked_users": "No has bloqueado a ningún usuario",
|
||||
"friend_code_copied": "Código de amigo copiado",
|
||||
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}",
|
||||
"privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración</0>.",
|
||||
"no_pending_invites": "No tenés invitaciones pendientes",
|
||||
"no_blocked_users": "No has bloqueado a nadie",
|
||||
"friend_code_copied": "Código de amistad copiado",
|
||||
"undo_friendship_modal_text": "Esto va a deshacer tu amistad con {{displayName}}",
|
||||
"privacy_hint": "Para cambiar quién puede ver esto, andá a <0>Ajustes</0>",
|
||||
"locked_profile": "Este perfil es privado",
|
||||
"image_process_failure": "Error al procesar la imagen",
|
||||
"required_field": "Este campo es obligatorio",
|
||||
"displayname_min_length": "El nombre a mostrar debe tener al menos 3 caracteres",
|
||||
"displayname_max_length": "El nombre a mostrar debe tener como máximo 50 caracteres",
|
||||
"image_process_failure": "Errpr al procesar la imagen",
|
||||
"required_field": "Este campo es requerido",
|
||||
"displayname_min_length": "El nombre en pantalla debe tener mínimo 3 caracteres",
|
||||
"displayname_max_length": "El nombre en pantalla debe tener máximo 50 caracteres",
|
||||
"report_profile": "Reportar este perfil",
|
||||
"report_reason": "¿Cual es el motivo del reporte?",
|
||||
"report_reason": "¿Porque estás reportando este perfil?",
|
||||
"report_description": "Información adicional",
|
||||
"report_description_placeholder": "Información adicional",
|
||||
"report": "Reportar",
|
||||
"report_reason_hate": "Discursos de odio",
|
||||
"report_reason_sexual_content": "Contenido sexual",
|
||||
"report_reason_violence": "Violencia",
|
||||
"report_reason_spam": "Spam / Contenido no deseado",
|
||||
"report_reason_other": "Otro",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Otros",
|
||||
"profile_reported": "Perfil reportado",
|
||||
"your_friend_code": "Tu código de amigo:",
|
||||
"upload_banner": "Subir un banner",
|
||||
"your_friend_code": "Tu código de amistad:",
|
||||
"upload_banner": "Subir banner",
|
||||
"uploading_banner": "Subiendo banner…",
|
||||
"background_image_updated": "Imagen de fondo actualizada",
|
||||
"playing": "Jugando {{game}}",
|
||||
"achievements": "logros",
|
||||
"achievements_unlocked": "Logros desbloqueados",
|
||||
"earned_points": "Puntos Obtenidos",
|
||||
"show_achievements_on_profile": "Mostrar tus logros en tu perfil",
|
||||
"show_points_on_profile": "Mostrar tus puntos obtenidos en tu perfil",
|
||||
"games": "Juegos",
|
||||
"ranking_updated_weekly": "El Ranking se actualiza semanalmente",
|
||||
"stats": "Estadísticas",
|
||||
"top_percentile": "Top {{percentile}}%"
|
||||
"achievements": "logros",
|
||||
"games": "Juegos",
|
||||
"top_percentile": "Top {{percentile}}%",
|
||||
"ranking_updated_weekly": "El ranking se actualiza semanalmente",
|
||||
"playing": "Jugando {{game}}",
|
||||
"achievements_unlocked": "Logros desbloqueados",
|
||||
"earned_points": "Puntos obtenidos",
|
||||
"show_achievements_on_profile": "Mostrá tus logros en tu perfil",
|
||||
"show_points_on_profile": "Mostrá los puntos obtenidos en tu perfil",
|
||||
"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"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Logro desbloqueado",
|
||||
"user_achievements": "Logros de {{displayName}}",
|
||||
"your_achievements": "Tus Logros",
|
||||
"your_achievements": "Tus logros",
|
||||
"unlocked_at": "Desbloqueado el: {{date}}",
|
||||
"subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido",
|
||||
"new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos",
|
||||
"subscription_needed": "Se requiere una suscripción a Hydra Cloud para ver esto",
|
||||
"new_achievements_unlocked": "Desbloqueaste {{achievementCount}} nuevos logros de {{gameCount}} juegos",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} logros",
|
||||
"achievements_unlocked_for_game": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}",
|
||||
"achievements_unlocked_for_game": "Desbloqueaste {{achievementCount}} nuevos logros para {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Este es un logro oculto",
|
||||
"achievement_earn_points": "Obtén {{points}} puntos con este logro",
|
||||
"achievement_earn_points": "Conseguí {{points}} puntos por este logro",
|
||||
"earned_points": "Puntos obtenidos:",
|
||||
"available_points": "Puntos disponibles:",
|
||||
"how_to_earn_achievements_points": "¿Cómo obtener puntos de logros?"
|
||||
"how_to_earn_achievements_points": "¿Como conseguir puntos por logros?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Suscripción Hydra Cloud",
|
||||
"debrid_description": "Descargas hasta x4 más rápidas con Nimbus",
|
||||
"subscribe_now": "Suscribirse ahora",
|
||||
"subscribe_now": "suscribíte ahora",
|
||||
"cloud_saving": "Guardado en la nube",
|
||||
"cloud_achievements": "Guarda tus logros en la nube",
|
||||
"animated_profile_picture": "Fotos de perfil animadas",
|
||||
"cloud_achievements": "Guardá tus logros en la nube",
|
||||
"animated_profile_picture": "Foto de perfil animada",
|
||||
"premium_support": "Soporte Premium",
|
||||
"show_and_compare_achievements": "Muestra y compara tus logros con otros usuarios",
|
||||
"animated_profile_banner": "Fondo de perfil animado",
|
||||
"show_and_compare_achievements": "Mostrá y compará tus logros con otros usuarios",
|
||||
"animated_profile_banner": "Banner de perfil animado",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "¡Has descubierto una característica de Hydra Cloud!",
|
||||
"learn_more": "Aprender más"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Edukalt sisse logitud"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Esile toodud",
|
||||
"surprise_me": "Üllata mind",
|
||||
"no_results": "Tulemusi ei leitud",
|
||||
"start_typing": "Alusta otsimiseks kirjutamist...",
|
||||
@@ -45,10 +44,7 @@
|
||||
"calculating_eta": "{{title}} allalaadimine… ({{percentage}} valmis) - Järelejäänud aja arvutamine…",
|
||||
"checking_files": "{{title}} failide kontrollimine… ({{percentage}} valmis)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Järgmine leht",
|
||||
"previous_page": "Eelmine leht"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Ava allalaadimise valikud",
|
||||
"download_options_zero": "Allalaadimise valikuid pole",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "فارسی",
|
||||
"home": {
|
||||
"featured": "پیشنهادی",
|
||||
"surprise_me": "سوپرایزم کن",
|
||||
"no_results": "اتمامای پیدا نشد"
|
||||
},
|
||||
@@ -17,7 +16,6 @@
|
||||
"home": "خانه",
|
||||
"favorites": "علاقهمندیها"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "جستجوی بازیها",
|
||||
"home": "خانه",
|
||||
@@ -31,10 +29,7 @@
|
||||
"downloading_metadata": "درحال دانلود متادیتاهای {{title}}…",
|
||||
"downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "صفحهی بعدی",
|
||||
"previous_page": "صفحهی قبلی"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "بازکردن آپشنهای دانلود",
|
||||
"download_options_zero": "هیچ آپشن دانلودی وجود ندارد",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Connecté avec succès"
|
||||
},
|
||||
"home": {
|
||||
"featured": "En vedette",
|
||||
"surprise_me": "Surprenez-moi",
|
||||
"no_results": "Aucun résultat trouvé",
|
||||
"start_typing": "Commencez à taper pour rechercher...",
|
||||
@@ -27,7 +26,8 @@
|
||||
"sign_in": "Se connecter",
|
||||
"friends": "Amis",
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"favorites": "Favoris"
|
||||
"favorites": "Favoris",
|
||||
"playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant"
|
||||
},
|
||||
"header": {
|
||||
"search": "Rechercher",
|
||||
@@ -356,7 +356,17 @@
|
||||
"common_redist_description": "Certains jeux nécessitent les redistribuables communs. L'installation est recommandée.",
|
||||
"install_common_redist": "Installer",
|
||||
"installing_common_redist": "Installation…",
|
||||
"show_download_speed_in_megabytes": "Afficher la vitesse de téléchargement en mégaoctets par seconde"
|
||||
"show_download_speed_in_megabytes": "Afficher la vitesse de téléchargement en mégaoctets par seconde",
|
||||
"extract_files_by_default": "Extraire les fichiers par défaut après le téléchargement",
|
||||
"enable_achievement_custom_notifications": "Activer les notifications personnalisées de succès",
|
||||
"achievement_custom_notification_position": "Position de la notification personnalisée de succès",
|
||||
"top-left": "En haut à gauche",
|
||||
"top-center": "En haut au centre",
|
||||
"top-right": "En haut à droite",
|
||||
"bottom-left": "En bas à gauche",
|
||||
"bottom-center": "En bas au centre",
|
||||
"bottom-right": "En bas à droite",
|
||||
"enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Téléchargement terminé",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "Magyar",
|
||||
"home": {
|
||||
"featured": "Featured",
|
||||
"surprise_me": "Lepj meg",
|
||||
"no_results": "Nem található"
|
||||
},
|
||||
@@ -19,7 +18,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Keresés",
|
||||
|
||||
"home": "Főoldal",
|
||||
"catalogue": "Katalógus",
|
||||
"downloads": "Letöltések",
|
||||
@@ -31,10 +29,7 @@
|
||||
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
||||
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Következő olda",
|
||||
"previous_page": "Előző olda"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Letöltési lehetőségek",
|
||||
"download_options_zero": "Nincs letöltési lehetőség",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Berhasil masuk"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Unggulan",
|
||||
"surprise_me": "Kejutkan saya",
|
||||
"no_results": "Tidak ada hasil ditemukan"
|
||||
},
|
||||
@@ -25,7 +24,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Cari game",
|
||||
|
||||
"home": "Beranda",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Unduhan",
|
||||
@@ -41,10 +39,7 @@
|
||||
"calculating_eta": "Mengunduh {{title}}… ({{percentage}} selesai) - Menghitung waktu yang tersisa…",
|
||||
"checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Halaman Berikutnya",
|
||||
"previous_page": "Halaman Sebelumnya"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Buka opsi unduhan",
|
||||
"download_options_zero": "Tidak ada opsi unduhan",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "Italiano",
|
||||
"home": {
|
||||
"featured": "In primo piano",
|
||||
"surprise_me": "Sorprendimi",
|
||||
"no_results": "Nessun risultato trovato"
|
||||
},
|
||||
@@ -15,11 +14,11 @@
|
||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||
"filter": "Filtra libreria",
|
||||
"home": "Home",
|
||||
"favorites": "Preferiti"
|
||||
"favorites": "Preferiti",
|
||||
"playable_button_title": "Mostra solo i giochi che puoi giocare ora"
|
||||
},
|
||||
"header": {
|
||||
"search": "Cerca",
|
||||
|
||||
"home": "Home",
|
||||
"catalogue": "Catalogo",
|
||||
"downloads": "Download",
|
||||
@@ -31,10 +30,7 @@
|
||||
"downloading_metadata": "Scaricamento metadati di {{title}}…",
|
||||
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Pagina successiva",
|
||||
"previous_page": "Pagina precedente"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Apri opzioni di download",
|
||||
"download_options_zero": "Nessuna opzione di download",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Сәтті кіру"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Ұсынылған",
|
||||
"surprise_me": "Таңқалдыр",
|
||||
"no_results": "Ештеңе табылмады"
|
||||
},
|
||||
@@ -23,7 +22,6 @@
|
||||
"sign_in": "Кіру",
|
||||
"favorites": "Таңдаулылар"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "Іздеу",
|
||||
"home": "Басты бет",
|
||||
@@ -40,10 +38,7 @@
|
||||
"downloading": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Аяқтау {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Қалған уақытты есептеу…"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Келесі бет",
|
||||
"previous_page": "Алдыңғы бет"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Жүктеу нұсқаларын ашу",
|
||||
"download_options_zero": "Жүктеу нұсқалары жоқ",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "한국어",
|
||||
"home": {
|
||||
"featured": "추천",
|
||||
"surprise_me": "무작위 추천",
|
||||
"no_results": "결과 없음"
|
||||
},
|
||||
@@ -17,7 +16,6 @@
|
||||
"home": "홈",
|
||||
"favorites": "즐겨찾기"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "게임 검색하기",
|
||||
"home": "홈",
|
||||
@@ -31,10 +29,7 @@
|
||||
"downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…",
|
||||
"downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "다음 페이지",
|
||||
"previous_page": "이전 페이지"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "다운로드 선택지 열기",
|
||||
"download_options_zero": "다운로드 선택지 없음",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Logget inn vellykket"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Anbefalinger",
|
||||
"surprise_me": "Overrask meg",
|
||||
"no_results": "Ingen resultater fundet",
|
||||
"start_typing": "Begynn å skrive for å søke...",
|
||||
@@ -29,7 +28,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Søk efter spill",
|
||||
|
||||
"home": "Hjem",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Nedlastinger",
|
||||
@@ -45,10 +43,7 @@
|
||||
"calculating_eta": "Laster ned {{title}}… ({{percentage}} ferdig) - Regner ut resterende tid…",
|
||||
"checking_files": "Sjekker {{title}} filer… ({{percentage}} ferdig)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Neste side",
|
||||
"previous_page": "Forrige side"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Åpne nedlastingsmuligheter",
|
||||
"download_options_zero": "Ingen nedlastingsmulighet",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "Nederlands",
|
||||
"home": {
|
||||
"featured": "Uitgelicht",
|
||||
"surprise_me": "Verrasing",
|
||||
"no_results": "Geen resultaten gevonden"
|
||||
},
|
||||
@@ -19,7 +18,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Zoek spellen",
|
||||
|
||||
"home": "Home",
|
||||
"catalogue": "Bibliotheek",
|
||||
"downloads": "Downloads",
|
||||
@@ -31,10 +29,7 @@
|
||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Volgende Pagina",
|
||||
"previous_page": "Vorige Pagina"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Open download Instellingen",
|
||||
"download_options_zero": "Geen download Instellingen",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "Polski",
|
||||
"home": {
|
||||
"featured": "Wyróżnione",
|
||||
"surprise_me": "Zaskocz mnie",
|
||||
"no_results": "Nie znaleziono wyników"
|
||||
},
|
||||
@@ -13,13 +12,13 @@
|
||||
"downloading_metadata": "{{title}} (Pobieranie metadata…)",
|
||||
"paused": "{{title}} (Zatrzymano)",
|
||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||
"filter": "Filtruj biblioteke",
|
||||
"filter": "Filtruj bibliotekę",
|
||||
"home": "Główna",
|
||||
"favorites": "Ulubione"
|
||||
"favorites": "Ulubione",
|
||||
"playable_button_title": "Pokaż tylko gry, w które możesz grać teraz"
|
||||
},
|
||||
"header": {
|
||||
"search": "Szukaj",
|
||||
|
||||
"home": "Główna",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Pobrane",
|
||||
@@ -31,10 +30,7 @@
|
||||
"downloading_metadata": "Pobieranie {{title}} metadata…",
|
||||
"downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Następna strona",
|
||||
"previous_page": "Poprzednia strona"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Otwórz opcje pobierania",
|
||||
"download_options_zero": "Brak opcji pobierania",
|
||||
|
||||
75
src/locales/pt-BR/translation.json
Normal file → Executable file
75
src/locales/pt-BR/translation.json
Normal file → Executable file
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Autenticado com sucesso"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destaques",
|
||||
"hot": "Populares",
|
||||
"weekly": "📅 Mais baixados da semana",
|
||||
"achievements": "🏆 Pra platinar",
|
||||
@@ -27,7 +26,22 @@
|
||||
"sign_in": "Login",
|
||||
"friends": "Amigos",
|
||||
"need_help": "Precisa de ajuda?",
|
||||
"favorites": "Favoritos"
|
||||
"favorites": "Favoritos",
|
||||
"add_custom_game_tooltip": "Adicionar jogo personalizado",
|
||||
"custom_game_modal": "Adicionar jogo personalizado",
|
||||
"edit_game_modal_title": "Título",
|
||||
"playable_button_title": "",
|
||||
"custom_game_modal_add": "Adicionar Jogo",
|
||||
"custom_game_modal_adding": "Adicionando...",
|
||||
"custom_game_modal_browse": "Buscar",
|
||||
"custom_game_modal_cancel": "Cancelar",
|
||||
"edit_game_modal_assets": "Imagens",
|
||||
"edit_game_modal_icon": "Ícone",
|
||||
"edit_game_modal_browse": "Buscar",
|
||||
"edit_game_modal_cancel": "Cancelar",
|
||||
"edit_game_modal_enter_title": "Insira o título",
|
||||
"edit_game_modal_logo": "Logo",
|
||||
"edit_game_modal": "Personalizar detalhes"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
@@ -76,6 +90,7 @@
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"accuracy": "{{accuracy}}% de precisão",
|
||||
"add_to_library": "Adicionar à biblioteca",
|
||||
"already_in_library": "Já está na biblioteca",
|
||||
"remove_from_library": "Remover da biblioteca",
|
||||
"no_downloads": "Nenhum download disponível",
|
||||
"play_time": "Jogou por {{amount}}",
|
||||
@@ -192,7 +207,45 @@
|
||||
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados",
|
||||
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
|
||||
"invalid_wine_prefix_path": "Caminho do prefixo Wine inválido",
|
||||
"invalid_wine_prefix_path_description": "O caminho para o prefixo Wine é inválido. Por favor, verifique o caminho e tente novamente."
|
||||
"invalid_wine_prefix_path_description": "O caminho para o prefixo Wine é inválido. Por favor, verifique o caminho e tente novamente.",
|
||||
"artifact_renamed": "Backup renomeado com sucesso",
|
||||
"rename_artifact": "Renomear Backup",
|
||||
"rename_artifact_description": "Renomeie o backup para um nome mais descritivo",
|
||||
"artifact_name_label": "Nome do backup",
|
||||
"artifact_name_placeholder": "Insira um nome para o backup",
|
||||
"save_changes": "Salvar mudanças",
|
||||
"required_field": "Este campo é obrigatório",
|
||||
"max_length_field": "Este campo deve ter menos de {{length}} caracteres",
|
||||
"freeze_backup": "Fixar para não ser apagado por backups automáticos",
|
||||
"unfreeze_backup": "Remover dos fixados",
|
||||
"backup_frozen": "Backup fixado",
|
||||
"backup_unfrozen": "Backup removido dos fixados",
|
||||
"backup_freeze_failed": "Falha ao fixar backup",
|
||||
"backup_freeze_failed_description": "Você deve deixar pelo menos um espaço livre para backups automáticos",
|
||||
"game_details": "Detalhes do Jogo",
|
||||
"currency_symbol": "R$",
|
||||
"currency_country": "br",
|
||||
"prices": "Preços",
|
||||
"no_prices_found": "Nenhum preço encontrado",
|
||||
"view_all_prices": "Clique para ver todos os preços",
|
||||
"retail_price": "Preço de lojas oficiais",
|
||||
"keyshop_price": "Preço em keyshops",
|
||||
"historical_retail": "Preço histórico de lojas oficiais",
|
||||
"historical_keyshop": "Preço histórico em keyshops",
|
||||
"language": "Idioma",
|
||||
"caption": "Legenda",
|
||||
"audio": "Áudio",
|
||||
"edit_game_modal_button": "Alterar detalhes do jogo",
|
||||
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||
"game_removed_from_pinned": "Jogo removido dos fixados",
|
||||
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
|
||||
"manual_playtime_warning": "As suas horas de jogo serão marcadas como atualizadas manualmente. Esta ação não pode ser desfeita.",
|
||||
"missing_wine_prefix": "Um prefixo Wine é necessário para criar um backup no Linux",
|
||||
"update_game_playtime": "Modificar tempo de jogo",
|
||||
"update_playtime": "Modificar tempo de jogo",
|
||||
"update_playtime_description": "Atualizar manualmente o tempo de jogo de {{game}}",
|
||||
"update_playtime_error": "Falha ao atualizar tempo de jogo",
|
||||
"update_playtime_title": "Atualizar tempo de jogo"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -350,6 +403,7 @@
|
||||
"installing_common_redist": "Instalando…",
|
||||
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
|
||||
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
|
||||
"enable_steam_achievements": "Habilitar busca por conquistas da Steam",
|
||||
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
|
||||
"top-left": "Superior esquerdo",
|
||||
"top-center": "Superior central",
|
||||
@@ -366,7 +420,8 @@
|
||||
"hidden": "Oculta",
|
||||
"test_notification": "Testar notificação",
|
||||
"notification_preview": "Prévia da Notificação de Conquistas",
|
||||
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
|
||||
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
|
||||
"editor_tab_code": "Código"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
@@ -493,7 +548,17 @@
|
||||
"achievements_unlocked": "Conquistas desbloqueadas",
|
||||
"earned_points": "Pontos ganhos",
|
||||
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
|
||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
|
||||
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
|
||||
"friend_code_length_error": "Código de amigo deve ter 8 caracteres",
|
||||
"top_percentile": "Top {{percentile}}%",
|
||||
"playtime": "Tempo de jogo",
|
||||
"played_recently": "Jogado recentemente",
|
||||
"pinned": "Fixado",
|
||||
"amount_minutes_short": "{{amount}}m",
|
||||
"amount_hours_short": "{{amount}}h",
|
||||
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||
"achievements_earned": "Conquistas recebidas"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Conquista desbloqueada",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Sessão iniciada com sucesso"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destaques",
|
||||
"hot": "Populares",
|
||||
"weekly": "📅 Mais descarregados esta semana",
|
||||
"achievements": "🏆 Para completar",
|
||||
@@ -26,7 +25,8 @@
|
||||
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
||||
"sign_in": "Iniciar sessão",
|
||||
"friends": "Amigos",
|
||||
"favorites": "Favoritos"
|
||||
"favorites": "Favoritos",
|
||||
"edit_game_modal_cancel": "Cancelar"
|
||||
},
|
||||
"header": {
|
||||
"search": "Procurar jogos",
|
||||
@@ -247,9 +247,6 @@
|
||||
"download_count_zero": "Sem downloads na lista",
|
||||
"download_count_one": "{{countFormatted}} download na lista",
|
||||
"download_count_other": "{{countFormatted}} downloads na lista",
|
||||
"download_options_zero": "Sem downloads disponíveis",
|
||||
"download_options_one": "{{countFormatted}} download disponível",
|
||||
"download_options_other": "{{countFormatted}} downloads disponíveis",
|
||||
"download_source_url": "URL da fonte",
|
||||
"add_download_source_description": "Insere o URL que contém o ficheiro .json",
|
||||
"download_source_up_to_date": "Sincronizada",
|
||||
@@ -359,8 +356,6 @@
|
||||
"instructions": "Verifica a forma correta de instalar algum deles na tua distribuição Linux, para garantir a execução normal do jogo"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Página seguinte",
|
||||
"previous_page": "Página anterior",
|
||||
"search": "Filtrar…",
|
||||
"developers": "Desenvolvedores",
|
||||
"genres": "Géneros",
|
||||
@@ -427,7 +422,6 @@
|
||||
"friend_code_copied": "Código de amigo copiado",
|
||||
"undo_friendship_modal_text": "Isto vai remover a tua amizade com {{displayName}}",
|
||||
"privacy_hint": "Para controlar quem pode ver o teu perfil, acede às <0>Definições</0>",
|
||||
"profile_locked": "Este perfil é privado",
|
||||
"image_process_failure": "Falha ao processar a imagem",
|
||||
"required_field": "Este campo é obrigatório",
|
||||
"displayname_min_length": "O nome de apresentação deve ter pelo menos 3 caracteres",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"language_name": "Română",
|
||||
"home": {
|
||||
"featured": "Recomandate",
|
||||
"surprise_me": "Surprinde-mă",
|
||||
"no_results": "Niciun rezultat găsit"
|
||||
},
|
||||
@@ -19,7 +18,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Caută jocuri",
|
||||
|
||||
"home": "Acasă",
|
||||
"catalogue": "Catalog",
|
||||
"downloads": "Descărcări",
|
||||
@@ -32,10 +30,7 @@
|
||||
"downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..."
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Pagina următoare",
|
||||
"previous_page": "Pagina anterioară"
|
||||
},
|
||||
"catalogue": {},
|
||||
"game_details": {
|
||||
"open_download_options": "Deschide opțiunile de descărcare",
|
||||
"download_options_zero": "Nicio opțiune de descărcare",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Успешный вход"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Рекомендации",
|
||||
"surprise_me": "Удиви меня",
|
||||
"no_results": "Ничего не найдено",
|
||||
"hot": "Сейчас популярно",
|
||||
@@ -27,7 +26,55 @@
|
||||
"sign_in": "Войти",
|
||||
"friends": "Друзья",
|
||||
"need_help": "Нужна помощь?",
|
||||
"favorites": "Избранное"
|
||||
"favorites": "Избранное",
|
||||
"playable_button_title": "Показать только установленные игры.",
|
||||
"custom_game_modal": "Добавить пользовательскую игру",
|
||||
"custom_game_modal_description": "Добавьте пользовательскую игру в библиотеку, выбрав исполняемый файл",
|
||||
"custom_game_modal_executable_path": "Путь к исполняемому файлу",
|
||||
"custom_game_modal_select_executable": "Выберите исполняемый файл",
|
||||
"custom_game_modal_title": "Название игры",
|
||||
"custom_game_modal_enter_title": "Введите название игры",
|
||||
"custom_game_modal_browse": "Обзор",
|
||||
"custom_game_modal_cancel": "Отмена",
|
||||
"custom_game_modal_add": "Добавить игру",
|
||||
"custom_game_modal_adding": "Добавление игры...",
|
||||
"custom_game_modal_success": "Пользовательская игра успешно добавлена",
|
||||
"custom_game_modal_failed": "Не удалось добавить пользовательскую игру",
|
||||
"custom_game_modal_executable": "Исполняемый файл",
|
||||
"edit_game_modal": "Настроить ресурсы",
|
||||
"edit_game_modal_description": "Настройте ресурсы и детали игры",
|
||||
"edit_game_modal_title": "Название",
|
||||
"edit_game_modal_enter_title": "Введите название",
|
||||
"edit_game_modal_image": "Изображение",
|
||||
"edit_game_modal_select_image": "Выберите изображение",
|
||||
"edit_game_modal_browse": "Обзор",
|
||||
"edit_game_modal_image_preview": "Предпросмотр изображения",
|
||||
"edit_game_modal_icon": "Иконка",
|
||||
"edit_game_modal_select_icon": "Выберите иконку",
|
||||
"edit_game_modal_icon_preview": "Предпросмотр иконки",
|
||||
"edit_game_modal_logo": "Логотип",
|
||||
"edit_game_modal_select_logo": "Выберите логотип",
|
||||
"edit_game_modal_logo_preview": "Предпросмотр логотипа",
|
||||
"edit_game_modal_hero": "Изображение обложку игры",
|
||||
"edit_game_modal_select_hero": "Выберите обложку игры",
|
||||
"edit_game_modal_hero_preview": "Предпросмотр обложки игры",
|
||||
"edit_game_modal_cancel": "Отмена",
|
||||
"edit_game_modal_update": "Обновить",
|
||||
"edit_game_modal_updating": "Обновление...",
|
||||
"edit_game_modal_fill_required": "Пожалуйста, заполните все обязательные поля",
|
||||
"edit_game_modal_success": "Ресурсы успешно обновлены",
|
||||
"edit_game_modal_failed": "Не удалось обновить ресурсы",
|
||||
"edit_game_modal_image_filter": "Изображение",
|
||||
"edit_game_modal_icon_resolution": "Рекомендуемое разрешение: 256x256px",
|
||||
"edit_game_modal_logo_resolution": "Рекомендуемое разрешение: 640x360px",
|
||||
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px",
|
||||
"edit_game_modal_assets": "Ресурсы",
|
||||
"edit_game_modal_drop_icon_image_here": "Перетащите изображение иконки сюда",
|
||||
"edit_game_modal_drop_logo_image_here": "Перетащите изображение логотипа сюда",
|
||||
"edit_game_modal_drop_hero_image_here": "Перетащите изображение обложки сюда",
|
||||
"edit_game_modal_drop_to_replace_icon": "Перетащите для замены иконки",
|
||||
"edit_game_modal_drop_to_replace_logo": "Перетащите для замены логотипа",
|
||||
"edit_game_modal_drop_to_replace_hero": "Перетащите для замены обложки"
|
||||
},
|
||||
"header": {
|
||||
"search": "Поиск",
|
||||
@@ -194,10 +241,37 @@
|
||||
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
|
||||
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
|
||||
"download_error_not_cached_on_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
|
||||
"update_playtime_title": "Обновить время игры",
|
||||
"update_playtime_description": "Вручную обновите время игры для {{game}}",
|
||||
"update_playtime": "Обновить время игры",
|
||||
"update_game_playtime": "Обновить время игры",
|
||||
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
|
||||
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||
"game_removed_from_favorites": "Игра удалена из избранного",
|
||||
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов"
|
||||
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов",
|
||||
"create_steam_shortcut": "Создать ярлык Steam",
|
||||
"you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения",
|
||||
"create_start_menu_shortcut": "Создать ярлык в меню «Пуск»",
|
||||
"invalid_wine_prefix_path": "Недопустимый путь префикса Wine",
|
||||
"invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.",
|
||||
"missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux",
|
||||
"download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.",
|
||||
"update_playtime_success": "Время игры успешно обновлено",
|
||||
"update_playtime_error": "Не удалось обновить время игры",
|
||||
"manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную. Это действие нельзя отменить.",
|
||||
"artifact_renamed": "Резервная копия успешно переименована",
|
||||
"rename_artifact": "Переименовать резервную копию",
|
||||
"rename_artifact_description": "Переименуйте резервную копию, присвоив ей более описательное имя.",
|
||||
"artifact_name_label": "Название резервной копии",
|
||||
"artifact_name_placeholder": "Введите название для резервной копии",
|
||||
"max_length_field": "Это поле должно содержать менее {{length}} символов",
|
||||
"freeze_backup": "Закрепить, чтобы она не была перезаписана автоматическими резервными копиями",
|
||||
"unfreeze_backup": "Открепить",
|
||||
"backup_frozen": "Резервная копия закреплена",
|
||||
"backup_unfrozen": "Резервная копия откреплена",
|
||||
"backup_freeze_failed": "Не удалось закрепить резервную копию",
|
||||
"backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий",
|
||||
"manual_playtime_tooltip": "Это время игры было обновлено вручную"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активировать Hydra",
|
||||
@@ -355,7 +429,26 @@
|
||||
"common_redist_description": "Для запуска некоторых игр требуются библиотеки. Во избежание проблем рекомендуется установить их.",
|
||||
"install_common_redist": "Установить",
|
||||
"installing_common_redist": "Установка…",
|
||||
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду"
|
||||
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
|
||||
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
|
||||
"achievement_custom_notification_position": "Позиция уведомлений достижений",
|
||||
"top-left": "Верхний левый угол",
|
||||
"top-center": "Верхний центр",
|
||||
"top-right": "Верхний правый угол",
|
||||
"bottom-left": "Нижний левый угол",
|
||||
"bottom-center": "Нижний центр",
|
||||
"bottom-right": "Нижний правый угол",
|
||||
"enable_achievement_custom_notifications": "Включить уведомления о достижениях",
|
||||
"alignment": "Выравнивание",
|
||||
"variation": "Вариация",
|
||||
"default": "По умолчанию",
|
||||
"rare": "Редкое",
|
||||
"platinum": "Платиновый",
|
||||
"hidden": "Скрытый",
|
||||
"test_notification": "Тестовое уведомление",
|
||||
"notification_preview": "Предварительный просмотр уведомления о достижении",
|
||||
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
|
||||
"enable_steam_achievements": "Включить поиск достижений Steam"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
@@ -368,9 +461,12 @@
|
||||
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
|
||||
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
|
||||
"new_friend_request_title": "Новый запрос на добавление в друзья",
|
||||
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья",
|
||||
"extraction_complete": "Распаковка завершена",
|
||||
"game_extracted": "{{title}} успешно распакован"
|
||||
"game_extracted": "{{title}} успешно распакован",
|
||||
"friend_started_playing_game": "{{displayName}} начал играть в игру",
|
||||
"test_achievement_notification_title": "Это тестовое уведомление",
|
||||
"test_achievement_notification_description": "Довольно круто, да?",
|
||||
"new_friend_request_description": "{{displayName}} отправил вам запрос в друзья"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Открыть Hydra",
|
||||
@@ -395,10 +491,13 @@
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} часов",
|
||||
"amount_minutes": "{{amount}} минут",
|
||||
"amount_hours_short": "{{amount}}ч",
|
||||
"amount_minutes_short": "{{amount}}м",
|
||||
"last_time_played": "Последняя игра {{period}}",
|
||||
"activity": "Недавняя активность",
|
||||
"library": "Библиотека",
|
||||
"total_play_time": "Всего сыграно",
|
||||
"manual_playtime_tooltip": "Время игры было обновлено вручную",
|
||||
"no_recent_activity_title": "Хммм... Тут ничего нет",
|
||||
"no_recent_activity_description": "Вы давно ни во что не играли. Пора это изменить!",
|
||||
"display_name": "Отображаемое имя",
|
||||
@@ -471,7 +570,9 @@
|
||||
"achievements_unlocked": "Достижения разблокированы",
|
||||
"earned_points": "Заработано очков:",
|
||||
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
||||
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
|
||||
"show_points_on_profile": "Показывать заработанные очки в своем профиле",
|
||||
"error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга",
|
||||
"friend_code_length_error": "Код друга должен содержать 8 символов"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Достижение разблокировано",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Inloggningen lyckades"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Utvalt",
|
||||
"surprise_me": "Överraska mig",
|
||||
"no_results": "Inga resultat hittades",
|
||||
"start_typing": "Börja skriva för att söka...",
|
||||
@@ -238,9 +237,9 @@
|
||||
"no_downloads_title": "Så tomt",
|
||||
"no_downloads_description": "Du har inte laddat ner något med Hydra än, men det är aldrig för sent att börja.",
|
||||
"checking_files": "Kontrollerar filer…",
|
||||
"seeding": "Delar",
|
||||
"stop_seeding": "Sluta dela",
|
||||
"resume_seeding": "Fortsätt dela",
|
||||
"seeding": "Seedar",
|
||||
"stop_seeding": "Sluta seeda",
|
||||
"resume_seeding": "Fortsätt seeda",
|
||||
"options": "Hantera",
|
||||
"extract": "Packa upp filer",
|
||||
"extracting": "Packar upp filer…"
|
||||
@@ -307,7 +306,7 @@
|
||||
"enable_achievement_notifications": "När en prestation låses upp",
|
||||
"launch_minimized": "Starta Hydra minimerad",
|
||||
"disable_nsfw_alert": "Inaktivera NSFW-varning",
|
||||
"seed_after_download_complete": "Dela efter att nedladdningen är klar",
|
||||
"seed_after_download_complete": "Seeda efter att nedladdningen är klar",
|
||||
"show_hidden_achievement_description": "Visa beskrivning av dolda prestationer innan de låses upp",
|
||||
"account": "Konto",
|
||||
"no_users_blocked": "Du har inga blockerade användare",
|
||||
@@ -379,7 +378,8 @@
|
||||
"platinum": "Platina",
|
||||
"hidden": "Dold",
|
||||
"test_notification": "Testa meddelande",
|
||||
"notification_preview": "Förhandsvisning av prestationmeddelande"
|
||||
"notification_preview": "Förhandsvisning av prestationmeddelande",
|
||||
"enable_friend_start_game_notifications": "När en vän börjar spela ett spel"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Nedladdning klar",
|
||||
|
||||
@@ -4,50 +4,49 @@
|
||||
"successfully_signed_in": "Başarıyla giriş yapıldı"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Öne Çıkanlar",
|
||||
"surprise_me": "Beni Şaşırt",
|
||||
"no_results": "Sonuç bulunamadı",
|
||||
"start_typing": "Aramak için yazmaya başlayın...",
|
||||
"hot": "Şu anda popüler",
|
||||
"weekly": "📅 Haftanın en iyi oyunları",
|
||||
"achievements": "🏆 Tamamlanacak oyunlar"
|
||||
"hot": "Şu anda Popüler",
|
||||
"weekly": "📅 Haftanın En İyi Oyunları",
|
||||
"achievements": "🏆 Bitirilecek Oyunlar"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirilenler",
|
||||
"settings": "Ayarlar",
|
||||
"my_library": "Kütüphane",
|
||||
"my_library": "Kütüphanem",
|
||||
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
|
||||
"paused": "{{title}} (Durduruldu)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphaneyi filtrele",
|
||||
"paused": "{{title}} (Duraklatıldı)",
|
||||
"downloading": "{{title}} (%{{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphanede filtrele",
|
||||
"home": "Ana Sayfa",
|
||||
"queued": "{{title}} (Sırada)",
|
||||
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
||||
"sign_in": "Giriş yap",
|
||||
"game_has_no_executable": "Bu oyun için çalıştırılabilir dosya seçilmedi",
|
||||
"sign_in": "Giriş Yap",
|
||||
"friends": "Arkadaşlar",
|
||||
"need_help": "Yardıma mı ihtiyacınız var?",
|
||||
"favorites": "Favoriler"
|
||||
},
|
||||
"header": {
|
||||
"search": "Oyunları ara",
|
||||
"search": "Oyunlarda Ara",
|
||||
"home": "Ana Sayfa",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirilenler",
|
||||
"search_results": "Arama sonuçları",
|
||||
"search_results": "Arama Sonuçları",
|
||||
"settings": "Ayarlar",
|
||||
"version_available_install": "{{version}} sürümü mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "{{version}} sürümü mevcut. İndirmek için buraya tıklayın."
|
||||
"version_available_install": "{{version}} sürümü mevcut. Yeniden başlatıp yüklemek için tıklayın.",
|
||||
"version_available_download": "{{version}} sürümü mevcut. İndirmek için tıklayın."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Devam eden indirme yok",
|
||||
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlanma: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
|
||||
"downloading": "{{title}} indiriliyor… (%{{percentage}} tamamlandı) - Bitiş: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… (%{{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… (%{{percentage}} tamamlandı)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "İndirme tamamlandı",
|
||||
"installation_complete_message": "Genel bağımlılıklar başarıyla yüklendi."
|
||||
"installation_complete": "Kurulum tamamlandı",
|
||||
"installation_complete_message": "Gerekli paketler başarıyla yüklendi"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filtrele…",
|
||||
@@ -58,7 +57,7 @@
|
||||
"download_sources": "İndirme kaynakları",
|
||||
"result_count": "{{resultCount}} sonuç",
|
||||
"filter_count": "{{filterCount}} mevcut",
|
||||
"clear_filters": "{{filterCount}} seçili filtreyi temizle"
|
||||
"clear_filters": "{{filterCount}} seçiliyi temizle"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "İndirme seçeneklerini aç",
|
||||
@@ -67,32 +66,32 @@
|
||||
"download_options_other": "{{count}} indirme seçeneği",
|
||||
"updated_at": "{{updated_at}} tarihinde güncellendi",
|
||||
"install": "Yükle",
|
||||
"resume": "Devam et",
|
||||
"pause": "Durdur",
|
||||
"cancel": "İptal et",
|
||||
"resume": "Devam Et",
|
||||
"pause": "Duraklat",
|
||||
"cancel": "İptal Et",
|
||||
"remove": "Kaldır",
|
||||
"space_left_on_disk": "Diskte {{space}} boş alan kaldı",
|
||||
"eta": "{{eta}} tahmini bitiş",
|
||||
"eta": "Bitiş: {{eta}}",
|
||||
"calculating_eta": "Kalan süre hesaplanıyor…",
|
||||
"downloading_metadata": "Meta veriler indiriliyor…",
|
||||
"filter": "Paketleri filtrele",
|
||||
"requirements": "Sistem gereksinimleri",
|
||||
"requirements": "Sistem Gereksinimleri",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Önerilen",
|
||||
"paused": "Durduruldu",
|
||||
"paused": "Duraklatıldı",
|
||||
"release_date": "{{date}} tarihinde yayımlandı",
|
||||
"publisher": "{{publisher}} tarafından yayımlandı",
|
||||
"hours": "saat",
|
||||
"minutes": "dakika",
|
||||
"amount_hours": "{{amount}} saat",
|
||||
"amount_minutes": "{{amount}} dakika",
|
||||
"accuracy": "{{accuracy}}% doğruluk",
|
||||
"accuracy": "%{{accuracy}} doğruluk",
|
||||
"add_to_library": "Kütüphaneye ekle",
|
||||
"remove_from_library": "Kütüphaneden kaldır",
|
||||
"no_downloads": "İndirilebilir içerik yok",
|
||||
"play_time": "{{amount}} süre oynandı",
|
||||
"last_time_played": "Son oynama {{period}} önce",
|
||||
"not_played_yet": "{{title}} henüz oynanmadı",
|
||||
"no_downloads": "İndirme mevcut değil",
|
||||
"play_time": "{{amount}} oynandı",
|
||||
"last_time_played": "Son oynanma: {{period}}",
|
||||
"not_played_yet": "{{title}} oyununu henüz oynamadınız",
|
||||
"next_suggestion": "Sonraki öneri",
|
||||
"play": "Oyna",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
@@ -107,134 +106,140 @@
|
||||
"download_path": "İndirme yolu",
|
||||
"previous_screenshot": "Önceki ekran görüntüsü",
|
||||
"next_screenshot": "Sonraki ekran görüntüsü",
|
||||
"screenshot": "{{number}} ekran görüntüsü",
|
||||
"open_screenshot": "{{number}} ekran görüntüsünü aç",
|
||||
"screenshot": "Ekran görüntüsü {{number}}",
|
||||
"open_screenshot": "Ekran görüntüsünü aç ({{number}})",
|
||||
"download_settings": "İndirme ayarları",
|
||||
"downloader": "İndirici",
|
||||
"select_executable": "Seç",
|
||||
"no_executable_selected": "Hiçbir çalıştırılabilir dosya seçilmedi",
|
||||
"no_executable_selected": "Çalıştırılabilir dosya seçilmedi",
|
||||
"open_folder": "Klasörü aç",
|
||||
"open_download_location": "İndirilen dosyaları gör",
|
||||
"open_download_location": "İndirilen dosyaları görüntüle",
|
||||
"create_shortcut": "Masaüstü kısayolu oluştur",
|
||||
"clear": "Temizle",
|
||||
"remove_files": "Dosyaları kaldır",
|
||||
"remove_from_library_title": "Emin misiniz?",
|
||||
"remove_from_library_description": "Bu işlem sonrasında {{game}} oyunu kütüphanenizden kaldıracaktır",
|
||||
"remove_from_library_description": "{{game}} oyununu kütüphanenizden kaldıracaktır",
|
||||
"options": "Seçenekler",
|
||||
"executable_section_title": "Çalıştırılabilir dosya",
|
||||
"executable_section_description": "\"Oyna\" butonuna tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"downloads_section_title": "İndirmeler",
|
||||
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
|
||||
"danger_zone_section_title": "Tehlike bölgesi",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden kaldırın veya Hydra tarafından indirilen dosyaları silin.",
|
||||
"download_in_progress": "İndirme devam ediyor",
|
||||
"download_paused": "İndirme durduruldu",
|
||||
"executable_section_description": "\"Oyna\" seçildiğinde çalışacak dosyanın yolu",
|
||||
"downloads_section_title": "İndirilenler",
|
||||
"downloads_section_description": "Bu oyunun güncelleme veya diğer sürümlerine göz atın",
|
||||
"danger_zone_section_title": "Tehlikeli Alan",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyalardan kaldırın",
|
||||
"download_in_progress": "İndirme sürüyor",
|
||||
"download_paused": "İndirme duraklatıldı",
|
||||
"last_downloaded_option": "Son indirilen seçenek",
|
||||
"create_steam_shortcut": "Steam kısayolu oluştur",
|
||||
"create_shortcut_success": "Kısayol başarıyla oluşturuldu",
|
||||
"you_might_need_to_restart_steam": "Değişiklikleri görmek için Steam'i yeniden başlatmanız gerekebilir",
|
||||
"create_shortcut_error": "Kısayol oluşturulurken hata oluştu",
|
||||
"nsfw_content_title": "Bu oyun uygunsuz içerik içeriyor",
|
||||
"nsfw_content_description": "{{title}} her yaş için uygun olmayabilecek içeriklere sahiptir. Devam etmek istediğinizden emin misiniz?",
|
||||
"nsfw_content_title": "Bu oyun uygunsuz içerik barındırıyor",
|
||||
"nsfw_content_description": "{{title}} bazı kullanıcılar için uygun olmayabilecek içerik barındırıyor. Devam etmek istediğinizden emin misiniz?",
|
||||
"allow_nsfw_content": "Devam et",
|
||||
"refuse_nsfw_content": "Geri dön",
|
||||
"stats": "İstatistikler",
|
||||
"download_count": "İndirme sayısı",
|
||||
"download_count": "İndirme",
|
||||
"player_count": "Aktif oyuncular",
|
||||
"download_error": "Bu indirme seçeneği mevcut değil",
|
||||
"download_error": "Bu indirme seçeneği kullanılamıyor",
|
||||
"download": "İndir",
|
||||
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
|
||||
"executable_path_in_use": "Çalıştırılabilir dosya zaten \"{{game}}\" tarafından kullanılıyor",
|
||||
"warning": "Uyarı:",
|
||||
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
|
||||
"hydra_needs_to_remain_open": "Bu indirme için, Hydra programının tamamlanana kadar açık kalması gerekir. Hydra kapanırsa, ilerlemeniz kaybolacaktır.",
|
||||
"achievements": "Başarımlar",
|
||||
"achievements_count": "Başarımlar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Bulut kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
|
||||
"cloud_save": "Bulut Kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazdan devam edin",
|
||||
"backups": "Yedekler",
|
||||
"install_backup": "Yükle",
|
||||
"delete_backup": "Sil",
|
||||
"create_backup": "Yeni yedek oluştur",
|
||||
"last_backup_date": "{{date}} tarihindeki son yedek",
|
||||
"no_backup_preview": "Bu oyun için bir kayıt dosyası bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
|
||||
"create_backup": "Yeni Yedek",
|
||||
"last_backup_date": "Son yedekleme: {{date}}",
|
||||
"no_backup_preview": "Bu başlık için kayıtlı oyun bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor (%{{progress}} tamamlandı)…",
|
||||
"uploading_backup": "Yedek yükleniyor…",
|
||||
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
|
||||
"no_backups": "Bu oyun için henüz yedek oluşturmadınız",
|
||||
"backup_uploaded": "Yedek yüklendi",
|
||||
"backup_deleted": "Yedek silindi",
|
||||
"backup_restored": "Yedek geri yüklendi",
|
||||
"see_all_achievements": "Tüm başarımları gör",
|
||||
"sign_in_to_see_achievements": "Başarımları görmek için oturum açın",
|
||||
"see_all_achievements": "Tüm başarımları görüntüle",
|
||||
"sign_in_to_see_achievements": "Başarımları görmek için giriş yapın",
|
||||
"mapping_method_automatic": "Otomatik",
|
||||
"mapping_method_manual": "Manuel",
|
||||
"mapping_method_label": "Eşleme yöntemi",
|
||||
"files_automatically_mapped": "Dosyalar otomatik olarak eşlendi",
|
||||
"no_backups_created": "Bu oyun için yedek oluşturulmadı",
|
||||
"mapping_method_label": "Eşleme metodu",
|
||||
"files_automatically_mapped": "Dosyalar otomatik eşlendi",
|
||||
"no_backups_created": "Bu oyun için oluşturulmuş yedek yok",
|
||||
"manage_files": "Dosyaları yönet",
|
||||
"loading_save_preview": "Kayıtlı oyunlar aranıyor…",
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
|
||||
"wine_prefix": "Wine Ön Ek",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine ön eki",
|
||||
"launch_options": "Başlatma Seçenekleri",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine parametreler girebilir (deneysel özellik)",
|
||||
"launch_options_placeholder": "Belirtilen bir parametre yok",
|
||||
"launch_options_description": "Gelişmiş kullanıcılar için başlatma parametreleri tanımlayın (deneysel özellik)",
|
||||
"launch_options_placeholder": "Parametre belirtilmedi",
|
||||
"no_download_option_info": "Bilgi mevcut değil",
|
||||
"backup_deletion_failed": "Yedek silinemedi",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarımlarınızı senkronize etmeyi öğrenin",
|
||||
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
|
||||
"backup_deletion_failed": "Yedek silme işlemi başarısız oldu",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için azami yedekleme sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarımlarını eşitlemeyi öğren",
|
||||
"manage_files_description": "Hangi dosyaların yedekleneceğini ve geri yükleneceğini yönetin",
|
||||
"select_folder": "Klasör seç",
|
||||
"backup_from": "{{date}} tarihinden yedek",
|
||||
"automatic_backup_from": "{{date}} tarihinden otomatik kayıt",
|
||||
"enable_automatic_cloud_sync": "Otomatik bulut kaydı senkronizasyonunu aktifleştir",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
|
||||
"no_directory_selected": "Bir dizin seçilmedi",
|
||||
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
|
||||
"backup_from": "{{date}} tarihli yedek",
|
||||
"automatic_backup_from": "{{date}} tarihli otomatik yedek",
|
||||
"enable_automatic_cloud_sync": "Otomatik bulut eşitlemesini etkinleştir",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu belirlendi",
|
||||
"no_directory_selected": "Klasör seçilmedi",
|
||||
"no_write_permission": "Bu klasöre indirme yapılamıyor. Detaylar için buraya tıklayın.",
|
||||
"reset_achievements": "Başarımları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarımları sıfırlar",
|
||||
"reset_achievements_description": "{{game}} için tüm başarımlar sıfırlanacak",
|
||||
"reset_achievements_title": "Emin misiniz?",
|
||||
"reset_achievements_success": "Başarımlar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarımlar sıfırlanamadı",
|
||||
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı doldurdunuz. Kotanın yenilenmesini bekleyin.",
|
||||
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirme işlemleri yapmak için yetkilendirilmemiş. Lütfen hesap ayarlarınızı kontrol edip tekrar deneyin.",
|
||||
"download_error_not_cached_on_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve Real-Debrid'den indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_on_torbox": "Bu indirme TorBox'ta mevcut değil ve TorBox'tan indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_on_hydra": "Bu indirme Nimbus'ta mevcut değil.",
|
||||
"game_removed_from_favorites": "Oyun favorilerden silindi",
|
||||
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı aştınız. Lütfen kotanın sıfırlanmasını bekleyin.",
|
||||
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirmeler için yetkili değil. Hesap ayarlarınızı kontrol edip tekrar deneyin.",
|
||||
"download_error_not_cached_on_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve durum sorgulanamıyor.",
|
||||
"download_error_not_cached_on_torbox": "Bu indirme TorBox üzerinde mevcut değil ve durum sorgulanamıyor.",
|
||||
"download_error_not_cached_on_hydra": "Bu indirme Nimbus üzerinde mevcut değil.",
|
||||
"game_removed_from_favorites": "Oyun favorilerden kaldırıldı",
|
||||
"game_added_to_favorites": "Oyun favorilere eklendi",
|
||||
"automatically_extract_downloaded_files": "Yüklenmiş dosyaları otomatik olarak çıkart"
|
||||
"automatically_extract_downloaded_files": "İndirilen dosyaları otomatik çıkart",
|
||||
"create_start_menu_shortcut": "Başlat Menüsüne kısayol oluştur",
|
||||
"invalid_wine_prefix_path": "Geçersiz Wine ön ek yolu",
|
||||
"invalid_wine_prefix_path_description": "Wine ön ek yolu hatalı. Lütfen yolu kontrol edin ve tekrar deneyin.",
|
||||
"missing_wine_prefix": "Linux'ta yedekleme oluşturmak için Wine ön eki gereklidir"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'yı Aktive Et",
|
||||
"installation_id": "Kurulum Kimliği:",
|
||||
"enter_activation_code": "Aktivasyon kodunuzu girin",
|
||||
"message": "Bunu nasıl edineceğini bilmiyorsan, buna sahip olmamalısın.",
|
||||
"activate": "Aktive Et",
|
||||
"title": "Hydra'yı Etkinleştir",
|
||||
"installation_id": "Kurulum ID:",
|
||||
"enter_activation_code": "Etkinleştirme kodunu girin",
|
||||
"message": "Bu kodun nereden alınacağını bilmiyorsanız, zaten bu kodu kullanmamanız gerekir.",
|
||||
"activate": "Etkinleştir",
|
||||
"loading": "Yükleniyor…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Devam Et",
|
||||
"pause": "Duraklat",
|
||||
"eta": "Tamamlama {{eta}}",
|
||||
"eta": "Bitiş: {{eta}}",
|
||||
"paused": "Duraklatıldı",
|
||||
"verifying": "Doğrulanıyor…",
|
||||
"completed": "Tamamlandı",
|
||||
"removed": "İndirilmedi",
|
||||
"cancel": "İptal Et",
|
||||
"filter": "İndirilen oyunları filtrele",
|
||||
"filter": "İndirilen oyunlarda filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Meta verileri indiriliyor…",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"delete": "Yükleyiciyi kaldır",
|
||||
"delete_modal_title": "Emin misiniz?",
|
||||
"delete_modal_description": "Bu işlem, tüm kurulum dosyalarını bilgisayarınızdan kaldıracaktır",
|
||||
"install": "Kur",
|
||||
"delete_modal_description": "Tüm kurulum dosyaları bilgisayarınızdan kaldırılacaktır",
|
||||
"install": "Yükle",
|
||||
"download_in_progress": "Devam ediyor",
|
||||
"queued_downloads": "Sıradaki indirmeler",
|
||||
"downloads_completed": "Tamamlananlar",
|
||||
"queued": "Sırada",
|
||||
"no_downloads_title": "Bomboş",
|
||||
"no_downloads_description": "Henüz Hydra ile hiçbir şey indirmediniz, ancak başlamak için asla geç değil.",
|
||||
"no_downloads_title": "Çok boş görünüyor",
|
||||
"no_downloads_description": "Hydra ile henüz bir şey indirmediniz, başlamak için asla geç değildir.",
|
||||
"checking_files": "Dosyalar kontrol ediliyor…",
|
||||
"seeding": "Paylaşılıyor",
|
||||
"stop_seeding": "Paylaşımı durdur",
|
||||
"resume_seeding": "Paylaşımı sürdür",
|
||||
"seeding": "Seed yapılıyor",
|
||||
"stop_seeding": "Seed yapmayı durdur",
|
||||
"resume_seeding": "Seed yapmaya devam et",
|
||||
"options": "Yönet",
|
||||
"extract": "Dosyaları çıkart",
|
||||
"extracting": "Dosyalar çıkartılıyor…"
|
||||
@@ -243,181 +248,202 @@
|
||||
"downloads_path": "İndirme yolu",
|
||||
"change": "Güncelle",
|
||||
"notifications": "Bildirimler",
|
||||
"enable_download_notifications": "Bir indirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
|
||||
"enable_download_notifications": "İndirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir paket eklendiğinde",
|
||||
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatınca sistem tepsisine gitmesin",
|
||||
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
|
||||
"quit_app_instead_hiding": "Hydra kapatıldığında gizleme",
|
||||
"launch_with_system": "Sistem başlatıldığında Hydra'yı aç",
|
||||
"general": "Genel",
|
||||
"behavior": "Davranış",
|
||||
"download_sources": "İndirme kaynakları",
|
||||
"language": "Dil",
|
||||
"api_token": "API Anahtarı",
|
||||
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
|
||||
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
|
||||
"enable_real_debrid": "Real-Debrid’i etkinleştir",
|
||||
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak dosyaları hızlı indirmenizi sağlayan sınırsız bir indirme servisidir.",
|
||||
"debrid_invalid_token": "Geçersiz API anahtarı",
|
||||
"debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
|
||||
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
|
||||
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsizdir. Lütfen Real-Debrid’e abone olun",
|
||||
"debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
||||
"save_changes": "Değişiklikleri Kaydet",
|
||||
"changes_saved": "Değişiklikler başarıyla kaydedildi",
|
||||
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
|
||||
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacaktır. Kaynak URL’si, bağlantıların bulunduğu bir .json dosyasına doğrudan bağlantı olmalıdır.",
|
||||
"validate_download_source": "Doğrula",
|
||||
"remove_download_source": "Kaldır",
|
||||
"add_download_source": "Kaynak ekle",
|
||||
"cancel_button_confirmation_delete_all_sources": "Hayır",
|
||||
"confirm_button_confirmation_delete_all_sources": "Evet, her şeyi sil",
|
||||
"description_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sileceksiniz",
|
||||
"title_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sil",
|
||||
"removed_download_sources": "Yazı tipleri kaldırıldı",
|
||||
"button_delete_all_sources": "Tüm indirme kaynaklarını kaldır",
|
||||
"download_count_zero": "İndirme seçeneği yok",
|
||||
"download_count_one": "{{countFormatted}} indirme seçeneği",
|
||||
"download_count_other": "{{countFormatted}} indirme seçeneği",
|
||||
"download_source_url": "İndirme kaynağı URL'si",
|
||||
"add_download_source_description": ".json dosyasının URL'sini girin",
|
||||
"add_download_source_description": ".json dosyasının URL’sini girin",
|
||||
"download_source_up_to_date": "Güncel",
|
||||
"download_source_errored": "Hatalı",
|
||||
"sync_download_sources": "Kaynakları senkronize et",
|
||||
"sync_download_sources": "Kaynakları eşitle",
|
||||
"removed_download_source": "İndirme kaynağı kaldırıldı",
|
||||
"removed_download_sources": "İndirme kaynakları kaldırıldı",
|
||||
"cancel_button_confirmation_delete_all_sources": "Hayır",
|
||||
"confirm_button_confirmation_delete_all_sources": "Evet, hepsini sil",
|
||||
"title_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sil",
|
||||
"description_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sileceksiniz",
|
||||
"button_delete_all_sources": "Tümünü kaldır",
|
||||
"added_download_source": "İndirme kaynağı eklendi",
|
||||
"download_sources_synced": "Tüm indirme kaynakları senkronize edildi",
|
||||
"insert_valid_json_url": "Geçerli bir JSON URL'si girin",
|
||||
"found_download_option_zero": "Hiçbir indirme seçeneği bulunamadı",
|
||||
"download_sources_synced": "Tüm indirme kaynakları eşitlendi",
|
||||
"insert_valid_json_url": "Geçerli bir JSON URL’si girin",
|
||||
"found_download_option_zero": "İndirme seçeneği bulunamadı",
|
||||
"found_download_option_one": "{{countFormatted}} indirme seçeneği bulundu",
|
||||
"found_download_option_other": "{{countFormatted}} indirme seçeneği bulundu",
|
||||
"import": "İçe aktar",
|
||||
"public": "Herkese açık",
|
||||
"import": "İçe Aktar",
|
||||
"public": "Herkese Açık",
|
||||
"private": "Gizli",
|
||||
"friends_only": "Sadece arkadaşlar",
|
||||
"friends_only": "Yalnızca Arkadaşlar",
|
||||
"privacy": "Gizlilik",
|
||||
"profile_visibility": "Profil görünürlüğü",
|
||||
"profile_visibility": "Profil Görünürlüğü",
|
||||
"profile_visibility_description": "Profilinizi ve kütüphanenizi kimlerin görebileceğini seçin",
|
||||
"required_field": "Bu alan gereklidir",
|
||||
"source_already_exists": "Bu kaynak zaten eklenmiş",
|
||||
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
|
||||
"source_already_exists": "Bu kaynak zaten eklendi",
|
||||
"must_be_valid_url": "Kaynak geçerli bir URL olmalı",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"user_unblocked": "Kullanıcının engeli kaldırıldı",
|
||||
"enable_achievement_notifications": "Bir başarım kilidi açıldığında",
|
||||
"launch_minimized": "Hydra'yı küçültülmüş başlat",
|
||||
"user_unblocked": "Kullanıcı engeli kaldırıldı",
|
||||
"enable_achievement_notifications": "Bir başarı açıldığında",
|
||||
"launch_minimized": "Hydra'yı küçük aç",
|
||||
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
|
||||
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
|
||||
"show_hidden_achievement_description": "Gizli başarım açıklamalarını kilitlenmeden önce göster",
|
||||
"seed_after_download_complete": "İndirme sonrası seed yap",
|
||||
"show_hidden_achievement_description": "Açılmadan önce gizli başarı açıklamasını göster",
|
||||
"account": "Hesap",
|
||||
"no_users_blocked": "Hiçbir kullanıcıyı engellemediniz",
|
||||
"subscription_active_until": "Hydra Cloud'unuz {{date}} tarihine kadar aktif",
|
||||
"subscription_active_until": "Hydra Cloud üyeliğiniz {{date}} tarihine kadar aktif",
|
||||
"manage_subscription": "Aboneliği yönet",
|
||||
"update_email": "E-posta'yı güncelle",
|
||||
"update_email": "E-postayı güncelle",
|
||||
"update_password": "Şifreyi güncelle",
|
||||
"current_email": "Aktif e-posta'nız",
|
||||
"no_email_account": "Henüz ayarlanmış bir e-postanız yok",
|
||||
"account_data_updated_successfully": "Hesap bilgileri başarıyla güncellendi",
|
||||
"current_email": "Mevcut e-posta:",
|
||||
"no_email_account": "Henüz bir e-posta tanımlanmadı",
|
||||
"account_data_updated_successfully": "Hesap verileri başarıyla güncellendi",
|
||||
"renew_subscription": "Hydra Cloud'u yenile",
|
||||
"subscription_expired_at": "Aboneliğiniz {{date}} tarihinde sona erdi",
|
||||
"no_subscription": "Hydra'yı en iyi şekilde deneyimleyin",
|
||||
"become_subscriber": "Hydra Cloud'lu ol",
|
||||
"subscription_renew_cancelled": "Otomatik yenileme devre dışı",
|
||||
"no_subscription": "Hydra'yı en iyi şekilde kullanın",
|
||||
"become_subscriber": "Hydra Cloud Ol",
|
||||
"subscription_renew_cancelled": "Otomatik yenileme devre dışı bırakıldı",
|
||||
"subscription_renews_on": "Aboneliğiniz {{date}} tarihinde yenilenecek",
|
||||
"bill_sent_until": "Bir sonraki faturanız bu tarihe kadar gönderilecek",
|
||||
"no_themes": "Henüz bir temanız yok gibi görünüyor, ama endişelenmeyin, ilk şaheserinizi oluşturmak için buraya tıklayın.",
|
||||
"bill_sent_until": "Sonraki fatura bu güne kadar gönderilecek",
|
||||
"no_themes": "Henüz bir temanız yok gibi görünüyor, endişelenmeyin, ilk şaheserinizi oluşturmak için buraya tıklayın.",
|
||||
"editor_tab_code": "Kod",
|
||||
"editor_tab_info": "Bilgi",
|
||||
"editor_tab_save": "Kaydet",
|
||||
"web_store": "İnternet mağazası",
|
||||
"web_store": "Web Mağaza",
|
||||
"clear_themes": "Temizle",
|
||||
"create_theme": "Oluştur",
|
||||
"create_theme_modal_title": "Tema oluştur",
|
||||
"create_theme_modal_description": "Hydra'nın görünümünü özelleştirmek için yeni bir tema oluştur",
|
||||
"theme_name": "İsim",
|
||||
"insert_theme_name": "Tema ismini gir",
|
||||
"set_theme": "Temayı seç",
|
||||
"unset_theme": "Tema seçimini kaldır",
|
||||
"create_theme_modal_title": "Özel tema oluştur",
|
||||
"create_theme_modal_description": "Hydra’nın görünümünü özelleştirmek için yeni bir tema oluşturun",
|
||||
"theme_name": "Tema adı",
|
||||
"insert_theme_name": "Tema adı girin",
|
||||
"set_theme": "Temayı ayarla",
|
||||
"unset_theme": "Temayı kaldır",
|
||||
"delete_theme": "Temayı sil",
|
||||
"edit_theme": "Temayı düzenle",
|
||||
"delete_all_themes": "Tüm temaları sil",
|
||||
"delete_all_themes_description": "Bu tüm temalarınızı silecektir",
|
||||
"delete_theme_description": "Bu {{theme}} temasını silecektir",
|
||||
"delete_all_themes_description": "Tüm özel temalarınız silinecek",
|
||||
"delete_theme_description": "{{theme}} teması silinecek",
|
||||
"cancel": "İptal",
|
||||
"appearance": "Görünüm",
|
||||
"enable_torbox": "TorBox'u etkinleştir",
|
||||
"torbox_description": "TorBox, piyasadaki en iyi sunucularla bile rekabet edebilen premium seedbox hizmetinizdir.",
|
||||
"enable_torbox": "TorBox'u Etkinleştir",
|
||||
"torbox_description": "TorBox, piyasadaki en iyi sunucularla yarışan premium seedbox hizmetinizdir.",
|
||||
"torbox_account_linked": "TorBox hesabı bağlandı",
|
||||
"create_real_debrid_account": "Henüz bir Real-Debrid hesabınız yoksa buraya tıklayın",
|
||||
"create_torbox_account": "Henüz bir TorBox hesabınız yoksa buraya tıklayın",
|
||||
"create_real_debrid_account": "Henüz Real Debrid hesabınız yoksa buraya tıklayın",
|
||||
"create_torbox_account": "Henüz TorBox hesabınız yoksa buraya tıklayın",
|
||||
"real_debrid_account_linked": "Real-Debrid hesabı bağlandı",
|
||||
"name_min_length": "Tema ismi en az 3 karakter uzunluğunda olmalıdır",
|
||||
"import_theme": "Temayı içe aktar",
|
||||
"import_theme_description": "{{theme}} teması, tema mağazasından içeri aktarılacak",
|
||||
"error_importing_theme": "Temayı içe aktarmada bir sorun oluştu",
|
||||
"name_min_length": "Tema adı en az 3 karakter olmalıdır",
|
||||
"import_theme": "Tema içe aktar",
|
||||
"import_theme_description": "{{theme}} temasını tema mağazasından içe aktaracaksınız",
|
||||
"error_importing_theme": "Tema içe aktarılırken hata oluştu",
|
||||
"theme_imported": "Tema başarıyla içe aktarıldı",
|
||||
"enable_friend_request_notifications": "Bir arkadaşlık isteği alındığında",
|
||||
"enable_auto_install": "Güncellemeleri otomatik yükle",
|
||||
"common_redist": "Ortak bağımlılıklar",
|
||||
"common_redist_description": "Bazı oyunların çalışabilmesi için genel bağımlılıklar gereklidir. Sorun yaşamamak için bunların yüklenmesi önerilir.",
|
||||
"install_common_redist": "Yükle",
|
||||
"installing_common_redist": "Yükleniyor…",
|
||||
"show_download_speed_in_megabytes": "İndirme hızını megabayt/saniye (MB/s) cinsinden göster"
|
||||
"enable_auto_install": "Güncellemeleri otomatik indir",
|
||||
"common_redist": "Gereksinim Paketleri",
|
||||
"common_redist_description": "Bazı oyunların çalışması için gereksinim paketleri gerekir. Sorun yaşamamak için kurulması önerilir.",
|
||||
"install_common_redist": "Kur",
|
||||
"installing_common_redist": "Kuruluyor…",
|
||||
"show_download_speed_in_megabytes": "İndirme hızını megabayt cinsinden göster",
|
||||
"extract_files_by_default": "İndirme sonrası varsayılan olarak dosyaları çıkar",
|
||||
"achievement_custom_notification_position": "Başarı özel bildirim konumu",
|
||||
"top-left": "Sol üst",
|
||||
"top-center": "Üst orta",
|
||||
"top-right": "Sağ üst",
|
||||
"bottom-left": "Sol alt",
|
||||
"bottom-center": "Alt orta",
|
||||
"bottom-right": "Sağ alt",
|
||||
"enable_achievement_custom_notifications": "Başarı özel bildirimlerini etkinleştir",
|
||||
"alignment": "Hizalama",
|
||||
"variation": "Çeşit",
|
||||
"default": "Varsayılan",
|
||||
"rare": "Nadir",
|
||||
"platinum": "Platin",
|
||||
"hidden": "Gizli",
|
||||
"test_notification": "Test bildirimi",
|
||||
"notification_preview": "Başarı Bildirimi Önizlemesi",
|
||||
"enable_friend_start_game_notifications": "Bir arkadaşınız oyun oynamaya başladığında"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "İndirme tamamlandı",
|
||||
"game_ready_to_install": "{{title}} kurulmaya hazır",
|
||||
"repack_list_updated": "Repack listesi güncellendi",
|
||||
"repack_count_one": "{{count}} repack eklendi",
|
||||
"repack_count_other": "{{count}} repack eklendi",
|
||||
"game_ready_to_install": "{{title}} yüklenmeye hazır",
|
||||
"repack_list_updated": "Paket listesi güncellendi",
|
||||
"repack_count_one": "{{count}} paket eklendi",
|
||||
"repack_count_other": "{{count}} paket eklendi",
|
||||
"new_update_available": "{{version}} sürümü mevcut",
|
||||
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarım kilidi açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarım açıldı",
|
||||
"new_friend_request_description": "Yeni bir arkadaşlık isteğin var",
|
||||
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra’yı yeniden başlatın",
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarı açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve {{count}} diğer başarı açıldı",
|
||||
"new_friend_request_description": "{{displayName}} size bir arkadaşlık isteği gönderdi",
|
||||
"new_friend_request_title": "Yeni arkadaşlık isteği",
|
||||
"extraction_complete": "Çıkartma tamamlandı",
|
||||
"game_extracted": "{{title}} başarıyla çıkartıldı"
|
||||
"extraction_complete": "Çıkarma tamamlandı",
|
||||
"game_extracted": "{{title}} başarıyla çıkarıldı",
|
||||
"friend_started_playing_game": "{{displayName}} bir oyun oynamaya başladı",
|
||||
"test_achievement_notification_title": "Bu bir test bildirimi",
|
||||
"test_achievement_notification_description": "Oldukça havalı, değil mi?"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra'yı Aç",
|
||||
"quit": "Çık"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "İndirilebilir içerik bulunmuyor",
|
||||
"available_one": "Mevcut",
|
||||
"available_other": "Mevcut"
|
||||
"available_other": "Mevcut",
|
||||
"no_downloads": "İndirme mevcut değil"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programlar Yüklü Değil",
|
||||
"description": "Wine veya Lutris çalıştırılabilir dosyaları sisteminizde bulunamadı",
|
||||
"instructions": "Oyunun normal çalışabilmesi için bunlardan herhangi birini Linux dağıtımınıza uygun şekilde nasıl kuracağınızı kontrol edin"
|
||||
"description": "Sisteminizde Wine veya Lutris çalıştırılabilir dosyaları bulunamadı",
|
||||
"instructions": "Oyunun sorunsuz çalışması için Linux dağıtımınızda bunların nasıl kurulacağını kontrol edin"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Kapat düğmesi"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Şifre görünürlüğünü değiştir"
|
||||
"toggle_password_visibility": "Şifreyi göster/gizle"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} saat",
|
||||
"amount_minutes": "{{amount}} dakika",
|
||||
"last_time_played": "Son oynanma {{period}}",
|
||||
"last_time_played": "Son oynanma: {{period}}",
|
||||
"activity": "Son Etkinlik",
|
||||
"library": "Kütüphane",
|
||||
"total_play_time": "Toplam oynama süresi",
|
||||
"no_recent_activity_title": "Hmmm… burada bir şey yok",
|
||||
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamamışsınız. Bunu değiştirmenin zamanı geldi!",
|
||||
"display_name": "Görünen isim",
|
||||
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamadınız. Bunu değiştirmenin zamanı geldi!",
|
||||
"display_name": "Kullanıcı adı",
|
||||
"saving": "Kaydediliyor",
|
||||
"save": "Kaydet",
|
||||
"edit_profile": "Profili Düzenle",
|
||||
"saved_successfully": "Başarıyla kaydedildi",
|
||||
"try_again": "Lütfen tekrar deneyin",
|
||||
"sign_out_modal_title": "Emin misiniz?",
|
||||
"sign_out_modal_title": "Çıkmak istediğinizden emin misiniz?",
|
||||
"cancel": "İptal",
|
||||
"successfully_signed_out": "Başarıyla çıkış yapıldı",
|
||||
"sign_out": "Çıkış yap",
|
||||
"playing_for": "{{amount}} oynanıyor",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Oturumu kapattığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Oturumu kapatmaya devam etmek istiyor musunuz?",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaparsanız, kütüphaneniz görünmeyecek ve ilerlemeniz kaydedilmeyecek. Yine de çıkış yapılsın mı?",
|
||||
"add_friends": "Arkadaş Ekle",
|
||||
"add": "Ekle",
|
||||
"friend_code": "Arkadaş kodu",
|
||||
"see_profile": "Profili gör",
|
||||
"see_profile": "Profili Görüntüle",
|
||||
"sending": "Gönderiliyor",
|
||||
"friend_request_sent": "Arkadaşlık isteği gönderildi",
|
||||
"friends": "Arkadaşlar",
|
||||
@@ -428,79 +454,79 @@
|
||||
"request_sent": "İstek gönderildi",
|
||||
"request_received": "İstek alındı",
|
||||
"accept_request": "İsteği kabul et",
|
||||
"ignore_request": "İsteği yok say",
|
||||
"ignore_request": "İsteği görmezden gel",
|
||||
"cancel_request": "İsteği iptal et",
|
||||
"undo_friendship": "Arkadaşlığı sonlandır",
|
||||
"undo_friendship": "Arkadaşlığı kaldır",
|
||||
"request_accepted": "İstek kabul edildi",
|
||||
"user_blocked_successfully": "Kullanıcı başarıyla engellendi",
|
||||
"user_block_modal_text": "Bu işlem {{displayName}} adlı kullanıcıyı engelleyecek",
|
||||
"user_block_modal_text": "{{displayName}} engellenecek",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"unblock": "Engeli kaldır",
|
||||
"no_friends_added": "Hiç arkadaş eklemediniz",
|
||||
"pending": "Bekliyor",
|
||||
"no_friends_added": "Hiç arkadaşınız yok",
|
||||
"pending": "Bekleyen",
|
||||
"no_pending_invites": "Bekleyen davetiniz yok",
|
||||
"no_blocked_users": "Engellenmiş kullanıcı yok",
|
||||
"no_blocked_users": "Engellenen kullanıcı yok",
|
||||
"friend_code_copied": "Arkadaş kodu kopyalandı",
|
||||
"undo_friendship_modal_text": "Bu işlem {{displayName}} ile arkadaşlığınızı sonlandıracak",
|
||||
"privacy_hint": "Bunu kimin görebileceğini ayarlamak için <0>Ayarlar</0> bölümüne gidin",
|
||||
"undo_friendship_modal_text": "Bu işlemle {{displayName}} ile arkadaşlığınız kaldırılacak",
|
||||
"privacy_hint": "Bunu kimlerin görebileceğini <0>Ayarlar</0> bölümünden değiştirebilirsiniz",
|
||||
"locked_profile": "Bu profil gizli",
|
||||
"image_process_failure": "Görüntü işleme başarısız oldu",
|
||||
"required_field": "Bu alan gerekli",
|
||||
"displayname_min_length": "Görünen isim en az 3 karakter uzunluğunda olmalıdır",
|
||||
"displayname_max_length": "Görünen isim en fazla 50 karakter uzunluğunda olabilir",
|
||||
"report_profile": "Bu profili bildir",
|
||||
"report_reason": "Bu profili neden bildiriyorsunuz?",
|
||||
"image_process_failure": "Resim işlenirken hata oluştu",
|
||||
"required_field": "Bu alan gereklidir",
|
||||
"displayname_min_length": "Kullanıcı adı en az 3 karakter olmalıdır",
|
||||
"displayname_max_length": "Kullanıcı adı en fazla 50 karakter olmalıdır",
|
||||
"report_profile": "Bu profili şikayet et",
|
||||
"report_reason": "Bu profili neden şikayet ediyorsunuz?",
|
||||
"report_description": "Ek bilgi",
|
||||
"report_description_placeholder": "Ek bilgi",
|
||||
"report": "Bildir",
|
||||
"report": "Şikayet et",
|
||||
"report_reason_hate": "Nefret söylemi",
|
||||
"report_reason_sexual_content": "Cinsel içerik",
|
||||
"report_reason_violence": "Şiddet",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Diğer",
|
||||
"profile_reported": "Profil bildirildi",
|
||||
"profile_reported": "Profil şikayet edildi",
|
||||
"your_friend_code": "Arkadaş kodunuz:",
|
||||
"upload_banner": "Afiş yükle",
|
||||
"uploading_banner": "Afiş yükleniyor…",
|
||||
"background_image_updated": "Arka plan görüntüsü güncellendi",
|
||||
"upload_banner": "Banner yükle",
|
||||
"uploading_banner": "Banner yükleniyor…",
|
||||
"background_image_updated": "Arka plan resmi güncellendi",
|
||||
"stats": "İstatistikler",
|
||||
"achievements": "Başarımlar",
|
||||
"games": "Oyunlar",
|
||||
"top_percentile": "En üst {{percentile}}%",
|
||||
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
|
||||
"top_percentile": "En iyi %{{percentile}}",
|
||||
"ranking_updated_weekly": "Sıralama haftalık güncellenir",
|
||||
"playing": "{{game}} oynanıyor",
|
||||
"achievements_unlocked": "Başarımlar açıldı",
|
||||
"achievements_unlocked": "Açılan başarımlar",
|
||||
"earned_points": "Kazanılan puanlar",
|
||||
"show_achievements_on_profile": "Başarımlarınızı profilinizde gösterin",
|
||||
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
|
||||
"show_achievements_on_profile": "Başarımlarını profilinde göster",
|
||||
"show_points_on_profile": "Kazanılan puanlarını profilinde göster"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Başarım açıldı",
|
||||
"user_achievements": "{{displayName}} oyununun Başarımları",
|
||||
"user_achievements": "{{displayName}}'nın Başarımları",
|
||||
"your_achievements": "Başarımlarınız",
|
||||
"unlocked_at": "Açılma zamanı: {{date}}",
|
||||
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarım açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarım",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarım açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarımdır",
|
||||
"achievement_earn_points": "Bu başarım ile {{points}} puan kazanın",
|
||||
"unlocked_at": "Açıldığı tarih: {{date}}",
|
||||
"subscription_needed": "Bu içeriği görmek için Hydra Cloud aboneliği gereklidir",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyunda {{achievementCount}} yeni başarı açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} için {{achievementCount}} yeni başarı açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
|
||||
"achievement_earn_points": "Bu başarı ile {{points}} puan kazan",
|
||||
"earned_points": "Kazanılan puanlar:",
|
||||
"available_points": "Mevcut puanlar:",
|
||||
"how_to_earn_achievements_points": "Başarım puanları nasıl kazanılır?"
|
||||
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud Aboneliği",
|
||||
"subscribe_now": "Şimdi abone olun",
|
||||
"cloud_saving": "Bulut kaydetme",
|
||||
"cloud_achievements": "Başarımlarınızı buluta kaydedin",
|
||||
"subscribe_now": "Şimdi abone ol",
|
||||
"cloud_saving": "Bulut kaydı",
|
||||
"cloud_achievements": "Başarımlarınızı bulutta saklayın",
|
||||
"animated_profile_picture": "Animasyonlu profil resimleri",
|
||||
"premium_support": "Premium Destek",
|
||||
"premium_support": "Öncelikli Destek",
|
||||
"show_and_compare_achievements": "Başarımlarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
|
||||
"animated_profile_banner": "Animasyonlu profil afişi",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
|
||||
"learn_more": "Daha Fazla Bilgi Edinin",
|
||||
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliğini keşfettiniz!",
|
||||
"learn_more": "Daha fazla bilgi al",
|
||||
"debrid_description": "Nimbus ile 4 kata kadar daha hızlı indirin"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Успішний вхід в систему"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Рекомендоване",
|
||||
"surprise_me": "Здивуй мене",
|
||||
"no_results": "Результатів не знайдено",
|
||||
"start_typing": "Почніть набирати текст для пошуку...",
|
||||
@@ -27,7 +26,8 @@
|
||||
"sign_in": "Увійти",
|
||||
"favorites": "Улюблені",
|
||||
"friends": "Друзі",
|
||||
"need_help": "Потрібна допомога?"
|
||||
"need_help": "Потрібна допомога?",
|
||||
"playable_button_title": "Показати лише ігри, які можна грати зараз"
|
||||
},
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"successfully_signed_in": "Kirish muvaffaqiyatli amalga oshirildi"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Tavsiya etilgan",
|
||||
"surprise_me": "Hayratda qoldir",
|
||||
"no_results": "Natijalar topilmadi",
|
||||
"hot": "Eng mashhur",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"language_name": "中文",
|
||||
"language_name": "简体中文",
|
||||
"app": {
|
||||
"successfully_signed_in": "已成功登录"
|
||||
},
|
||||
"home": {
|
||||
"featured": "特色推荐",
|
||||
"surprise_me": "向我推荐",
|
||||
"no_results": "没有找到结果",
|
||||
"start_typing": "键入以开始搜素...",
|
||||
@@ -26,7 +25,9 @@
|
||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||
"sign_in": "登入",
|
||||
"friends": "好友",
|
||||
"favorites": "收藏"
|
||||
"favorites": "收藏",
|
||||
"need_help": "需要帮助?",
|
||||
"playable_button_title": "仅显示现在可以游玩的游戏"
|
||||
},
|
||||
"header": {
|
||||
"search": "搜索游戏",
|
||||
@@ -43,11 +44,21 @@
|
||||
"downloading_metadata": "正在下载{{title}}的元数据…",
|
||||
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}",
|
||||
"calculating_eta": "正在下载 {{title}}… (已完成{{percentage}}.) - 正在计算剩余时间...",
|
||||
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)"
|
||||
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)",
|
||||
"installation_complete": "安装完成",
|
||||
"installation_complete_message": "通用可再发行组件安装成功",
|
||||
"installing_common_redist": "{{log}}…"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "下一页",
|
||||
"previous_page": "上一页"
|
||||
"clear_filters": "清除已选的 {{filterCount}} 项",
|
||||
"developers": "开发商",
|
||||
"download_sources": "下载源",
|
||||
"filter_count": "{{filterCount}} 项可用",
|
||||
"genres": "类型",
|
||||
"publishers": "发行商",
|
||||
"result_count": "{{resultCount}} 个结果",
|
||||
"search": "筛选…",
|
||||
"tags": "标签"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "打开下载菜单",
|
||||
@@ -166,7 +177,48 @@
|
||||
"manage_files_description": "管理哪些文件要备份和恢复",
|
||||
"select_folder": "选择文件夹",
|
||||
"backup_from": "{{date}} 时备份",
|
||||
"custom_backup_location_set": "自定义备份文件位置"
|
||||
"custom_backup_location_set": "自定义备份文件位置",
|
||||
"artifact_name_label": "备份名称",
|
||||
"artifact_name_placeholder": "为备份输入名称",
|
||||
"artifact_renamed": "备份重命名成功",
|
||||
"automatic_backup_from": "{{date}} 的自动备份",
|
||||
"automatically_extract_downloaded_files": "自动解压下载的文件",
|
||||
"backup_freeze_failed": "固定备份失败",
|
||||
"backup_freeze_failed_description": "您必须至少保留一个空位用于自动备份",
|
||||
"backup_frozen": "备份已固定",
|
||||
"backup_unfrozen": "备份已取消固定",
|
||||
"clear": "清除",
|
||||
"create_start_menu_shortcut": "创建开始菜单快捷方式",
|
||||
"create_steam_shortcut": "创建Steam快捷方式",
|
||||
"download_error_gofile_quota_exceeded": "您已超出Gofile的月度配额。请等待配额重置。",
|
||||
"download_error_not_cached_on_hydra": "此下载在Nimbus上不可用。",
|
||||
"download_error_not_cached_on_real_debrid": "此下载在Real-Debrid上不可用,且暂不支持从Real-Debrid轮询下载状态。",
|
||||
"download_error_not_cached_on_torbox": "此下载在TorBox上不可用,且暂不支持从TorBox轮询下载状态。",
|
||||
"download_error_real_debrid_account_not_authorized": "您的Real-Debrid账户未被授权进行新下载。请检查您的账户设置并重试。",
|
||||
"enable_automatic_cloud_sync": "启用自动云同步",
|
||||
"freeze_backup": "固定以免被自动备份覆盖",
|
||||
"game_added_to_favorites": "游戏已添加到收藏",
|
||||
"game_removed_from_favorites": "游戏已从收藏中移除",
|
||||
"invalid_wine_prefix_path": "无效的Wine前置路径",
|
||||
"invalid_wine_prefix_path_description": "Wine前置的路径无效。请检查路径并重试。",
|
||||
"launch_options": "启动选项",
|
||||
"launch_options_description": "高级用户可以选择修改启动选项(实验性功能)",
|
||||
"launch_options_placeholder": "未指定参数",
|
||||
"max_length_field": "此字段必须少于 {{length}} 个字符",
|
||||
"missing_wine_prefix": "在Linux上创建备份需要Wine前置",
|
||||
"no_directory_selected": "未选择目录",
|
||||
"no_write_permission": "无法下载到此目录。点击此处了解更多。",
|
||||
"rename_artifact": "重命名备份",
|
||||
"rename_artifact_description": "将备份重命名为更具描述性的名称",
|
||||
"required_field": "此字段为必填项",
|
||||
"reset_achievements": "重置成就",
|
||||
"reset_achievements_description": "这将重置 {{game}} 的所有成就",
|
||||
"reset_achievements_error": "重置成就失败",
|
||||
"reset_achievements_success": "成就重置成功",
|
||||
"reset_achievements_title": "您确定吗?",
|
||||
"save_changes": "保存更改",
|
||||
"unfreeze_backup": "取消固定",
|
||||
"you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改"
|
||||
},
|
||||
"activation": {
|
||||
"title": "激活 Hydra",
|
||||
@@ -199,7 +251,13 @@
|
||||
"queued": "下载列表",
|
||||
"no_downloads_title": "空空如也",
|
||||
"no_downloads_description": "你还未使用Hydra下载任何游戏,但什么时候开始,都为时不晚。",
|
||||
"checking_files": "正在校验文件…"
|
||||
"checking_files": "正在校验文件…",
|
||||
"extract": "解压文件",
|
||||
"extracting": "正在解压文件…",
|
||||
"options": "管理",
|
||||
"resume_seeding": "恢复做种",
|
||||
"seeding": "做种中",
|
||||
"stop_seeding": "停止做种"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "下载路径",
|
||||
@@ -260,7 +318,83 @@
|
||||
"must_be_valid_url": "来源必须是有效的 URL",
|
||||
"blocked_users": "已屏蔽用户",
|
||||
"user_unblocked": "用户已经被屏蔽",
|
||||
"enable_achievement_notifications": "当成就解锁时"
|
||||
"enable_achievement_notifications": "当成就解锁时",
|
||||
"account": "账户",
|
||||
"account_data_updated_successfully": "账户数据更新成功",
|
||||
"achievement_custom_notification_position": "成就自定义通知位置",
|
||||
"alignment": "对齐",
|
||||
"appearance": "外观",
|
||||
"become_subscriber": "成为Hydra Cloud用户",
|
||||
"bill_sent_until": "您的下一张账单将在此日期前发送",
|
||||
"bottom-center": "底部中央",
|
||||
"bottom-left": "底部左侧",
|
||||
"bottom-right": "底部右侧",
|
||||
"cancel": "取消",
|
||||
"clear_themes": "清除",
|
||||
"common_redist": "通用可再发行组件",
|
||||
"common_redist_description": "运行某些游戏需要通用可再发行组件。建议安装以避免问题。",
|
||||
"create_real_debrid_account": "如果您还没有Real-Debrid账户,请点击此处",
|
||||
"create_theme": "创建",
|
||||
"create_theme_modal_description": "创建新主题以自定义Hydra的外观",
|
||||
"create_theme_modal_title": "创建自定义主题",
|
||||
"create_torbox_account": "如果您还没有TorBox账户,请点击此处",
|
||||
"current_email": "当前邮箱:",
|
||||
"default": "默认",
|
||||
"delete_all_themes": "删除所有主题",
|
||||
"delete_all_themes_description": "这将删除所有您的自定义主题",
|
||||
"delete_theme": "删除主题",
|
||||
"delete_theme_description": "这将删除主题 {{theme}}",
|
||||
"disable_nsfw_alert": "禁用NSFW警告",
|
||||
"edit_theme": "编辑主题",
|
||||
"editor_tab_code": "代码",
|
||||
"editor_tab_info": "信息",
|
||||
"editor_tab_save": "保存",
|
||||
"enable_achievement_custom_notifications": "启用成就自定义通知",
|
||||
"enable_auto_install": "自动下载更新",
|
||||
"enable_friend_request_notifications": "当收到好友请求时",
|
||||
"enable_friend_start_game_notifications": "当好友开始游戏时",
|
||||
"enable_torbox": "启用TorBox",
|
||||
"error_importing_theme": "导入主题时出错",
|
||||
"extract_files_by_default": "下载后默认解压文件",
|
||||
"hidden": "隐藏",
|
||||
"import_theme": "导入主题",
|
||||
"import_theme_description": "您将从主题商店导入 {{theme}}",
|
||||
"insert_theme_name": "输入主题名称",
|
||||
"install_common_redist": "安装",
|
||||
"installing_common_redist": "正在安装…",
|
||||
"launch_minimized": "最小化启动Hydra",
|
||||
"manage_subscription": "管理订阅",
|
||||
"name_min_length": "主题名称必须至少3个字符长",
|
||||
"no_email_account": "您尚未设置邮箱",
|
||||
"no_subscription": "以最佳方式享受Hydra",
|
||||
"no_themes": "看起来您还没有任何主题,但别担心,点击这里创建您的第一个杰作。",
|
||||
"no_users_blocked": "您没有屏蔽任何用户",
|
||||
"notification_preview": "成就通知预览",
|
||||
"platinum": "白金",
|
||||
"rare": "稀有",
|
||||
"real_debrid_account_linked": "Real-Debrid账户已连接",
|
||||
"renew_subscription": "续费Hydra Cloud",
|
||||
"seed_after_download_complete": "下载完成后做种",
|
||||
"set_theme": "设置主题",
|
||||
"show_download_speed_in_megabytes": "以兆字节每秒显示下载速度",
|
||||
"show_hidden_achievement_description": "在解锁前显示隐藏成就描述",
|
||||
"subscription_active_until": "您的Hydra Cloud活跃至 {{date}}",
|
||||
"subscription_expired_at": "您的订阅已于 {{date}} 到期",
|
||||
"subscription_renew_cancelled": "自动续费已禁用",
|
||||
"subscription_renews_on": "您的订阅将于 {{date}} 续费",
|
||||
"test_notification": "测试通知",
|
||||
"theme_imported": "主题导入成功",
|
||||
"theme_name": "名称",
|
||||
"top-center": "顶部中央",
|
||||
"top-left": "顶部左侧",
|
||||
"top-right": "顶部右侧",
|
||||
"torbox_account_linked": "TorBox账户已连接",
|
||||
"torbox_description": "TorBox是您的高级种子盒服务,甚至可与市场上最好的服务器相媲美。",
|
||||
"unset_theme": "取消设置主题",
|
||||
"update_email": "更新邮箱",
|
||||
"update_password": "更新密码",
|
||||
"variation": "变体",
|
||||
"web_store": "网络商店"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "下载完成",
|
||||
@@ -271,14 +405,23 @@
|
||||
"new_update_available": "版本 {{version}} 可用",
|
||||
"restart_to_install_update": "重启 Hydra 以安装更新",
|
||||
"notification_achievement_unlocked_title": "{{game}} 的成绩已解锁",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} 和其他 {{count}} 已解锁"
|
||||
"notification_achievement_unlocked_body": "{{achievement}} 和其他 {{count}} 已解锁",
|
||||
"extraction_complete": "解压完成",
|
||||
"friend_started_playing_game": "{{displayName}} 开始玩游戏",
|
||||
"game_extracted": "{{title}} 解压成功",
|
||||
"new_friend_request_description": "{{displayName}} 向您发送了好友请求",
|
||||
"new_friend_request_title": "新好友请求",
|
||||
"test_achievement_notification_description": "非常酷,对吧?",
|
||||
"test_achievement_notification_title": "这是一个测试通知"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "打开 Hydra",
|
||||
"quit": "退出"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "无可用下载选项"
|
||||
"no_downloads": "无可用下载选项",
|
||||
"available_one": "可用",
|
||||
"available_other": "可用"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "程序未安装",
|
||||
@@ -351,7 +494,7 @@
|
||||
"report_description": "额外信息",
|
||||
"report_description_placeholder": "额外信息",
|
||||
"report": "举报",
|
||||
"report_reason_hate": "Hate speech",
|
||||
"report_reason_hate": "仇恨言论",
|
||||
"report_reason_sexual_content": "色情内容",
|
||||
"report_reason_violence": "暴力",
|
||||
"report_reason_spam": "骚扰",
|
||||
@@ -360,7 +503,19 @@
|
||||
"your_friend_code": "你的好友代码:",
|
||||
"upload_banner": "上传横幅",
|
||||
"uploading_banner": "上传横幅中…",
|
||||
"background_image_updated": "背景图片已更新"
|
||||
"background_image_updated": "背景图片已更新",
|
||||
"achievements": "成就",
|
||||
"achievements_unlocked": "成就已解锁",
|
||||
"earned_points": "获得积分",
|
||||
"error_adding_friend": "无法发送好友请求。请检查好友代码",
|
||||
"friend_code_length_error": "好友代码必须为8个字符",
|
||||
"games": "游戏",
|
||||
"playing": "正在玩 {{game}}",
|
||||
"ranking_updated_weekly": "排名每周更新",
|
||||
"show_achievements_on_profile": "在您的个人资料上显示成就",
|
||||
"show_points_on_profile": "在您的个人资料上显示获得的积分",
|
||||
"stats": "统计",
|
||||
"top_percentile": "前 {{percentile}}%"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "成就已解锁",
|
||||
@@ -368,7 +523,14 @@
|
||||
"your_achievements": "你的成就",
|
||||
"unlocked_at": "解锁于: {{date}}",
|
||||
"subscription_needed": "需要订阅 Hydra Cloud 才能看到此内容",
|
||||
"new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就"
|
||||
"new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就",
|
||||
"achievement_earn_points": "通过此成就获得 {{points}} 积分",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} 成就",
|
||||
"achievements_unlocked_for_game": "为 {{gameTitle}} 解锁了 {{achievementCount}} 个新成就",
|
||||
"available_points": "可用积分:",
|
||||
"earned_points": "获得积分:",
|
||||
"hidden_achievement_tooltip": "这是一个隐藏成就",
|
||||
"how_to_earn_achievements_points": "如何获得成就积分?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra 云订阅",
|
||||
@@ -378,6 +540,10 @@
|
||||
"animated_profile_picture": "动画头像",
|
||||
"premium_support": "高级技术支持",
|
||||
"show_and_compare_achievements": "展示并与其他用户比较您的成就",
|
||||
"animated_profile_banner": "动态个人简介横幅"
|
||||
"animated_profile_banner": "动态个人简介横幅",
|
||||
"debrid_description": "使用Nimbus下载速度提升4倍",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "您刚刚发现了一个Hydra Cloud功能!",
|
||||
"learn_more": "了解更多"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,10 @@ export const commonRedistPath = path.join(
|
||||
"CommonRedist"
|
||||
);
|
||||
|
||||
export const logsPath = path.join(SystemPath.getPath("userData"), "logs");
|
||||
|
||||
export const seedsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "seeds")
|
||||
: path.join(__dirname, "..", "..", "seeds");
|
||||
export const logsPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
`logs${isStaging ? "-staging" : ""}`
|
||||
);
|
||||
|
||||
export const achievementSoundPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "achievement.wav")
|
||||
@@ -42,4 +41,4 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
||||
|
||||
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
|
||||
|
||||
export const MAIN_LOOP_INTERVAL = 1500;
|
||||
export const MAIN_LOOP_INTERVAL = 2000;
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import type { GameShop, GameStats } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { gamesStatsCacheSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes
|
||||
|
||||
const getGameStats = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => {
|
||||
const cachedStats = await gamesStatsCacheSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
if (
|
||||
cachedStats &&
|
||||
cachedStats.updatedAt + LOCAL_CACHE_EXPIRATION > Date.now()
|
||||
) {
|
||||
return cachedStats;
|
||||
}
|
||||
|
||||
return HydraApi.get<GameStats>(
|
||||
`/games/stats`,
|
||||
{ objectId, shop },
|
||||
{ needsAuth: false }
|
||||
);
|
||||
).then(async (data) => {
|
||||
await gamesStatsCacheSublevel.put(levelKeys.game(shop, objectId), {
|
||||
...data,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("getGameStats", getGameStats);
|
||||
|
||||
@@ -8,7 +8,18 @@ const saveGameShopAssets = async (
|
||||
shop: GameShop,
|
||||
assets: ShopAssets
|
||||
): Promise<void> => {
|
||||
return gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), assets);
|
||||
const key = levelKeys.game(shop, objectId);
|
||||
const existingAssets = await gamesShopAssetsSublevel.get(key);
|
||||
|
||||
// Preserve existing title if it differs from the incoming title (indicating it was customized)
|
||||
const shouldPreserveTitle =
|
||||
existingAssets?.title && existingAssets.title !== assets.title;
|
||||
|
||||
return gamesShopAssetsSublevel.put(key, {
|
||||
...existingAssets,
|
||||
...assets,
|
||||
title: shouldPreserveTitle ? existingAssets.title : assets.title,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
||||
|
||||
14
src/main/events/cloud-save/rename-game-artifact.ts
Normal file
14
src/main/events/cloud-save/rename-game-artifact.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const renameGameArtifact = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameArtifactId: string,
|
||||
label: string
|
||||
) => {
|
||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}`, {
|
||||
label,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("renameGameArtifact", renameGameArtifact);
|
||||
16
src/main/events/cloud-save/toggle-artifact-freeze.ts
Normal file
16
src/main/events/cloud-save/toggle-artifact-freeze.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const toggleArtifactFreeze = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameArtifactId: string,
|
||||
freeze: boolean
|
||||
) => {
|
||||
if (freeze) {
|
||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/freeze`);
|
||||
} else {
|
||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/unfreeze`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("toggleArtifactFreeze", toggleArtifactFreeze);
|
||||
@@ -14,8 +14,12 @@ import "./catalogue/get-developers";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/add-custom-game-to-library";
|
||||
import "./library/update-custom-game";
|
||||
import "./library/update-game-custom-assets";
|
||||
import "./library/add-game-to-favorites";
|
||||
import "./library/remove-game-from-favorites";
|
||||
import "./library/toggle-game-pin";
|
||||
import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
@@ -33,9 +37,12 @@ import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./library/select-game-wine-prefix";
|
||||
import "./library/reset-game-achievements";
|
||||
import "./library/change-game-playtime";
|
||||
import "./library/toggle-automatic-cloud-sync";
|
||||
import "./library/get-default-wine-prefix-selection-path";
|
||||
import "./library/cleanup-unused-assets";
|
||||
import "./library/create-steam-shortcut";
|
||||
import "./library/copy-custom-game-asset";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
@@ -44,6 +51,8 @@ import "./misc/show-item-in-folder";
|
||||
import "./misc/get-badges";
|
||||
import "./misc/install-common-redist";
|
||||
import "./misc/can-install-common-redist";
|
||||
import "./misc/save-temp-file";
|
||||
import "./misc/delete-temp-file";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./torrenting/resume-game-download";
|
||||
@@ -63,6 +72,7 @@ import "./auth/sign-out";
|
||||
import "./auth/open-auth-window";
|
||||
import "./auth/get-session-hash";
|
||||
import "./user/get-user";
|
||||
import "./user/get-user-library";
|
||||
import "./user/get-blocked-users";
|
||||
import "./user/block-user";
|
||||
import "./user/unblock-user";
|
||||
@@ -86,6 +96,8 @@ import "./cloud-save/get-game-backup-preview";
|
||||
import "./cloud-save/upload-save-game";
|
||||
import "./cloud-save/delete-game-artifact";
|
||||
import "./cloud-save/select-game-backup-path";
|
||||
import "./cloud-save/toggle-artifact-freeze";
|
||||
import "./cloud-save/rename-game-artifact";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import "./notifications/update-achievement-notification-window";
|
||||
import "./notifications/show-achievement-test-notification";
|
||||
|
||||
65
src/main/events/library/add-custom-game-to-library.ts
Normal file
65
src/main/events/library/add-custom-game-to-library.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import { randomUUID } from "crypto";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const addCustomGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
title: string,
|
||||
executablePath: string,
|
||||
iconUrl?: string,
|
||||
logoImageUrl?: string,
|
||||
libraryHeroImageUrl?: string
|
||||
) => {
|
||||
const objectId = randomUUID();
|
||||
const shop: GameShop = "custom";
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const existingGames = await gamesSublevel.iterator().all();
|
||||
const existingGame = existingGames.find(
|
||||
([_key, game]) => game.executablePath === executablePath && !game.isDeleted
|
||||
);
|
||||
|
||||
if (existingGame) {
|
||||
throw new Error(
|
||||
"A game with this executable path already exists in your library"
|
||||
);
|
||||
}
|
||||
|
||||
const assets = {
|
||||
objectId,
|
||||
shop,
|
||||
title,
|
||||
iconUrl: iconUrl || null,
|
||||
libraryHeroImageUrl: libraryHeroImageUrl || "",
|
||||
libraryImageUrl: iconUrl || "",
|
||||
logoImageUrl: logoImageUrl || "",
|
||||
logoPosition: null,
|
||||
coverImageUrl: iconUrl || "",
|
||||
};
|
||||
await gamesShopAssetsSublevel.put(gameKey, assets);
|
||||
|
||||
const game = {
|
||||
title,
|
||||
iconUrl: iconUrl || null,
|
||||
logoImageUrl: logoImageUrl || null,
|
||||
libraryHeroImageUrl: libraryHeroImageUrl || null,
|
||||
objectId,
|
||||
shop,
|
||||
remoteId: null,
|
||||
isDeleted: false,
|
||||
playTimeInMilliseconds: 0,
|
||||
lastTimePlayed: null,
|
||||
executablePath,
|
||||
launchOptions: null,
|
||||
favorite: false,
|
||||
automaticCloudSync: false,
|
||||
hasManuallyUpdatedPlaytime: false,
|
||||
};
|
||||
|
||||
await gamesSublevel.put(gameKey, game);
|
||||
|
||||
return game;
|
||||
};
|
||||
|
||||
registerEvent("addCustomGameToLibrary", addCustomGameToLibrary);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const addGameToFavorites = async (
|
||||
@@ -12,6 +13,8 @@ const addGameToFavorites = async (
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameShop } from "@types";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||
import {
|
||||
downloadsSublevel,
|
||||
gamesShopAssetsSublevel,
|
||||
gamesSublevel,
|
||||
levelKeys,
|
||||
} from "@main/level";
|
||||
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
|
||||
|
||||
const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -30,6 +30,8 @@ const addGameToLibrary = async (
|
||||
game = {
|
||||
title,
|
||||
iconUrl: gameAssets?.iconUrl ?? null,
|
||||
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
|
||||
logoImageUrl: gameAssets?.logoImageUrl ?? null,
|
||||
objectId,
|
||||
shop,
|
||||
remoteId: null,
|
||||
@@ -41,9 +43,14 @@ const addGameToLibrary = async (
|
||||
await gamesSublevel.put(gameKey, game);
|
||||
}
|
||||
|
||||
await createGame(game).catch(() => {});
|
||||
if (game) {
|
||||
await createGame(game).catch(() => {});
|
||||
|
||||
updateLocalUnlockedAchievements(game);
|
||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("addGameToLibrary", addGameToLibrary);
|
||||
|
||||
33
src/main/events/library/change-game-playtime.ts
Normal file
33
src/main/events/library/change-game-playtime.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const changeGamePlaytime = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
playTimeInSeconds: number
|
||||
) => {
|
||||
try {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
if (game.remoteId) {
|
||||
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
|
||||
playTimeInSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
playTimeInMilliseconds: playTimeInSeconds * 1000,
|
||||
hasManuallyUpdatedPlaytime: true,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update game playtime: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("changeGamePlayTime", changeGamePlaytime);
|
||||
76
src/main/events/library/cleanup-unused-assets.ts
Normal file
76
src/main/events/library/cleanup-unused-assets.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { ipcMain } from "electron";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ASSETS_PATH } from "@main/constants";
|
||||
|
||||
const getCustomGamesAssetsPath = () => {
|
||||
return path.join(ASSETS_PATH, "custom-games");
|
||||
};
|
||||
|
||||
const getAllCustomGameAssets = async (): Promise<string[]> => {
|
||||
const assetsPath = getCustomGamesAssetsPath();
|
||||
|
||||
if (!fs.existsSync(assetsPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = await fs.promises.readdir(assetsPath);
|
||||
return files.map((file) => path.join(assetsPath, file));
|
||||
};
|
||||
|
||||
const getUsedAssetPaths = async (): Promise<Set<string>> => {
|
||||
// Get all custom games from the level database
|
||||
const { gamesSublevel } = await import("@main/level");
|
||||
const allGames = await gamesSublevel.iterator().all();
|
||||
|
||||
const customGames = allGames
|
||||
.map(([_key, game]) => game)
|
||||
.filter((game) => game.shop === "custom" && !game.isDeleted);
|
||||
|
||||
const usedPaths = new Set<string>();
|
||||
|
||||
customGames.forEach((game) => {
|
||||
// Extract file paths from local URLs
|
||||
if (game.iconUrl?.startsWith("local:")) {
|
||||
usedPaths.add(game.iconUrl.replace("local:", ""));
|
||||
}
|
||||
if (game.logoImageUrl?.startsWith("local:")) {
|
||||
usedPaths.add(game.logoImageUrl.replace("local:", ""));
|
||||
}
|
||||
if (game.libraryHeroImageUrl?.startsWith("local:")) {
|
||||
usedPaths.add(game.libraryHeroImageUrl.replace("local:", ""));
|
||||
}
|
||||
});
|
||||
|
||||
return usedPaths;
|
||||
};
|
||||
|
||||
export const cleanupUnusedAssets = async (): Promise<{
|
||||
deletedCount: number;
|
||||
errors: string[];
|
||||
}> => {
|
||||
try {
|
||||
const allAssets = await getAllCustomGameAssets();
|
||||
const usedAssets = await getUsedAssetPaths();
|
||||
|
||||
const errors: string[] = [];
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const assetPath of allAssets) {
|
||||
if (!usedAssets.has(assetPath)) {
|
||||
try {
|
||||
await fs.promises.unlink(assetPath);
|
||||
deletedCount++;
|
||||
} catch (error) {
|
||||
errors.push(`Failed to delete ${assetPath}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { deletedCount, errors };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to cleanup unused assets: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
ipcMain.handle("cleanupUnusedAssets", cleanupUnusedAssets);
|
||||
42
src/main/events/library/copy-custom-game-asset.ts
Normal file
42
src/main/events/library/copy-custom-game-asset.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "crypto";
|
||||
import { ASSETS_PATH } from "@main/constants";
|
||||
|
||||
const copyCustomGameAsset = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
sourcePath: string,
|
||||
assetType: "icon" | "logo" | "hero"
|
||||
): Promise<string> => {
|
||||
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
||||
throw new Error("Source file does not exist");
|
||||
}
|
||||
|
||||
// Ensure assets directory exists
|
||||
if (!fs.existsSync(ASSETS_PATH)) {
|
||||
fs.mkdirSync(ASSETS_PATH, { recursive: true });
|
||||
}
|
||||
|
||||
// Create custom games assets subdirectory
|
||||
const customGamesAssetsPath = path.join(ASSETS_PATH, "custom-games");
|
||||
if (!fs.existsSync(customGamesAssetsPath)) {
|
||||
fs.mkdirSync(customGamesAssetsPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Get file extension
|
||||
const fileExtension = path.extname(sourcePath);
|
||||
|
||||
// Generate unique filename
|
||||
const uniqueId = randomUUID();
|
||||
const fileName = `${assetType}-${uniqueId}${fileExtension}`;
|
||||
const destinationPath = path.join(customGamesAssetsPath, fileName);
|
||||
|
||||
// Copy the file
|
||||
await fs.promises.copyFile(sourcePath, destinationPath);
|
||||
|
||||
// Return the local URL format
|
||||
return `local:${destinationPath}`;
|
||||
};
|
||||
|
||||
registerEvent("copyCustomGameAsset", copyCustomGameAsset);
|
||||
@@ -94,7 +94,7 @@ const createSteamShortcut = async (
|
||||
|
||||
if (!steamUserIds.length) {
|
||||
logger.error("No Steam user ID found");
|
||||
return;
|
||||
throw new Error("No Steam user ID found");
|
||||
}
|
||||
|
||||
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =
|
||||
|
||||
@@ -23,7 +23,10 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||
...game,
|
||||
download: download ?? null,
|
||||
...gameAssets,
|
||||
};
|
||||
// Ensure compatibility with LibraryGame type
|
||||
libraryHeroImageUrl:
|
||||
game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl,
|
||||
} as LibraryGame;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const removeGameFromFavorites = async (
|
||||
@@ -12,6 +13,8 @@ const removeGameFromFavorites = async (
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(() => {});
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
|
||||
@@ -1,7 +1,70 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop, Game } from "@types";
|
||||
import fs from "node:fs";
|
||||
|
||||
const collectAssetPathsToDelete = (game: Game): string[] => {
|
||||
const assetPathsToDelete: string[] = [];
|
||||
|
||||
const assetUrls =
|
||||
game.shop === "custom"
|
||||
? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl]
|
||||
: [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl];
|
||||
|
||||
for (const url of assetUrls) {
|
||||
if (url?.startsWith("local:")) {
|
||||
assetPathsToDelete.push(url.replace("local:", ""));
|
||||
}
|
||||
}
|
||||
|
||||
return assetPathsToDelete;
|
||||
};
|
||||
|
||||
const updateGameAsDeleted = async (
|
||||
game: Game,
|
||||
gameKey: string
|
||||
): Promise<void> => {
|
||||
const updatedGame = {
|
||||
...game,
|
||||
isDeleted: true,
|
||||
executablePath: null,
|
||||
...(game.shop !== "custom" && {
|
||||
customIconUrl: null,
|
||||
customLogoImageUrl: null,
|
||||
customHeroImageUrl: null,
|
||||
}),
|
||||
};
|
||||
|
||||
await gamesSublevel.put(gameKey, updatedGame);
|
||||
};
|
||||
|
||||
const resetShopAssets = async (gameKey: string): Promise<void> => {
|
||||
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||
if (existingAssets) {
|
||||
const resetAssets = {
|
||||
...existingAssets,
|
||||
title: existingAssets.title,
|
||||
};
|
||||
await gamesShopAssetsSublevel.put(gameKey, resetAssets);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAssetFiles = async (
|
||||
assetPathsToDelete: string[]
|
||||
): Promise<void> => {
|
||||
if (assetPathsToDelete.length === 0) return;
|
||||
|
||||
for (const assetPath of assetPathsToDelete) {
|
||||
try {
|
||||
if (fs.existsSync(assetPath)) {
|
||||
await fs.promises.unlink(assetPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete asset ${assetPath}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeGameFromLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -11,17 +74,21 @@ const removeGameFromLibrary = async (
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isDeleted: true,
|
||||
executablePath: null,
|
||||
});
|
||||
if (!game) return;
|
||||
|
||||
if (game?.remoteId) {
|
||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||
}
|
||||
const assetPathsToDelete = collectAssetPathsToDelete(game);
|
||||
|
||||
await updateGameAsDeleted(game, gameKey);
|
||||
|
||||
if (game.shop !== "custom") {
|
||||
await resetShopAssets(gameKey);
|
||||
}
|
||||
|
||||
if (game?.remoteId) {
|
||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||
}
|
||||
|
||||
await deleteAssetFiles(assetPathsToDelete);
|
||||
};
|
||||
|
||||
registerEvent("removeGameFromLibrary", removeGameFromLibrary);
|
||||
|
||||
@@ -16,7 +16,8 @@ const resetGameAchievements = async (
|
||||
objectId: string
|
||||
) => {
|
||||
try {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
const levelKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(levelKey);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
@@ -29,8 +30,6 @@ const resetGameAchievements = async (
|
||||
}
|
||||
}
|
||||
|
||||
const levelKey = levelKeys.game(game.shop, game.objectId);
|
||||
|
||||
await gameAchievementsSublevel
|
||||
.get(levelKey)
|
||||
.then(async (gameAchievements) => {
|
||||
|
||||
43
src/main/events/library/toggle-game-pin.ts
Normal file
43
src/main/events/library/toggle-game-pin.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import type { GameShop, UserGame } from "@types";
|
||||
|
||||
const toggleGamePin = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
pin: boolean
|
||||
) => {
|
||||
try {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
if (pin) {
|
||||
const response = await HydraApi.put<UserGame>(
|
||||
`/profile/games/${shop}/${objectId}/pin`
|
||||
);
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isPinned: pin,
|
||||
pinnedDate: new Date(response.pinnedDate!),
|
||||
});
|
||||
} else {
|
||||
await HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`);
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isPinned: pin,
|
||||
pinnedDate: null,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to update game pinned status", error);
|
||||
throw new Error(`Failed to update game pinned status: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("toggleGamePin", toggleGamePin);
|
||||
98
src/main/events/library/update-custom-game.ts
Normal file
98
src/main/events/library/update-custom-game.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
import fs from "node:fs";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
interface UpdateCustomGameParams {
|
||||
shop: GameShop;
|
||||
objectId: string;
|
||||
title: string;
|
||||
iconUrl?: string;
|
||||
logoImageUrl?: string;
|
||||
libraryHeroImageUrl?: string;
|
||||
originalIconPath?: string;
|
||||
originalLogoPath?: string;
|
||||
originalHeroPath?: string;
|
||||
}
|
||||
|
||||
const updateCustomGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
params: UpdateCustomGameParams
|
||||
) => {
|
||||
const {
|
||||
shop,
|
||||
objectId,
|
||||
title,
|
||||
iconUrl,
|
||||
logoImageUrl,
|
||||
libraryHeroImageUrl,
|
||||
originalIconPath,
|
||||
originalLogoPath,
|
||||
originalHeroPath,
|
||||
} = params;
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const existingGame = await gamesSublevel.get(gameKey);
|
||||
if (!existingGame) {
|
||||
throw new Error("Game not found");
|
||||
}
|
||||
|
||||
const oldAssetPaths: string[] = [];
|
||||
|
||||
const assetPairs = [
|
||||
{ existing: existingGame.iconUrl, new: iconUrl },
|
||||
{ existing: existingGame.logoImageUrl, new: logoImageUrl },
|
||||
{ existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl },
|
||||
];
|
||||
|
||||
for (const { existing, new: newUrl } of assetPairs) {
|
||||
if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) {
|
||||
oldAssetPaths.push(existing.replace("local:", ""));
|
||||
}
|
||||
}
|
||||
|
||||
const updatedGame = {
|
||||
...existingGame,
|
||||
title,
|
||||
iconUrl: iconUrl || null,
|
||||
logoImageUrl: logoImageUrl || null,
|
||||
libraryHeroImageUrl: libraryHeroImageUrl || null,
|
||||
originalIconPath: originalIconPath || existingGame.originalIconPath || null,
|
||||
originalLogoPath: originalLogoPath || existingGame.originalLogoPath || null,
|
||||
originalHeroPath: originalHeroPath || existingGame.originalHeroPath || null,
|
||||
};
|
||||
|
||||
await gamesSublevel.put(gameKey, updatedGame);
|
||||
|
||||
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||
if (existingAssets) {
|
||||
const updatedAssets = {
|
||||
...existingAssets,
|
||||
title,
|
||||
iconUrl: iconUrl || null,
|
||||
libraryHeroImageUrl: libraryHeroImageUrl || "",
|
||||
libraryImageUrl: iconUrl || "",
|
||||
logoImageUrl: logoImageUrl || "",
|
||||
coverImageUrl: iconUrl || "",
|
||||
};
|
||||
|
||||
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
|
||||
}
|
||||
|
||||
if (oldAssetPaths.length > 0) {
|
||||
for (const assetPath of oldAssetPaths) {
|
||||
try {
|
||||
if (fs.existsSync(assetPath)) {
|
||||
await fs.promises.unlink(assetPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete old asset ${assetPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedGame;
|
||||
};
|
||||
|
||||
registerEvent("updateCustomGame", updateCustomGame);
|
||||
162
src/main/events/library/update-game-custom-assets.ts
Normal file
162
src/main/events/library/update-game-custom-assets.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop, Game } from "@types";
|
||||
import fs from "node:fs";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
const collectOldAssetPaths = (
|
||||
existingGame: Game,
|
||||
customIconUrl?: string | null,
|
||||
customLogoImageUrl?: string | null,
|
||||
customHeroImageUrl?: string | null
|
||||
): string[] => {
|
||||
const oldAssetPaths: string[] = [];
|
||||
|
||||
const assetPairs = [
|
||||
{ existing: existingGame.customIconUrl, new: customIconUrl },
|
||||
{ existing: existingGame.customLogoImageUrl, new: customLogoImageUrl },
|
||||
{ existing: existingGame.customHeroImageUrl, new: customHeroImageUrl },
|
||||
];
|
||||
|
||||
for (const { existing, new: newUrl } of assetPairs) {
|
||||
if (
|
||||
existing &&
|
||||
newUrl !== undefined &&
|
||||
existing !== newUrl &&
|
||||
existing.startsWith("local:")
|
||||
) {
|
||||
oldAssetPaths.push(existing.replace("local:", ""));
|
||||
}
|
||||
}
|
||||
|
||||
return oldAssetPaths;
|
||||
};
|
||||
|
||||
interface UpdateGameDataParams {
|
||||
gameKey: string;
|
||||
existingGame: Game;
|
||||
title: string;
|
||||
customIconUrl?: string | null;
|
||||
customLogoImageUrl?: string | null;
|
||||
customHeroImageUrl?: string | null;
|
||||
customOriginalIconPath?: string | null;
|
||||
customOriginalLogoPath?: string | null;
|
||||
customOriginalHeroPath?: string | null;
|
||||
}
|
||||
|
||||
const updateGameData = async (params: UpdateGameDataParams): Promise<Game> => {
|
||||
const {
|
||||
gameKey,
|
||||
existingGame,
|
||||
title,
|
||||
customIconUrl,
|
||||
customLogoImageUrl,
|
||||
customHeroImageUrl,
|
||||
customOriginalIconPath,
|
||||
customOriginalLogoPath,
|
||||
customOriginalHeroPath,
|
||||
} = params;
|
||||
const updatedGame = {
|
||||
...existingGame,
|
||||
title,
|
||||
...(customIconUrl !== undefined && { customIconUrl }),
|
||||
...(customLogoImageUrl !== undefined && { customLogoImageUrl }),
|
||||
...(customHeroImageUrl !== undefined && { customHeroImageUrl }),
|
||||
...(customOriginalIconPath !== undefined && { customOriginalIconPath }),
|
||||
...(customOriginalLogoPath !== undefined && { customOriginalLogoPath }),
|
||||
...(customOriginalHeroPath !== undefined && { customOriginalHeroPath }),
|
||||
};
|
||||
|
||||
await gamesSublevel.put(gameKey, updatedGame);
|
||||
return updatedGame;
|
||||
};
|
||||
|
||||
const updateShopAssets = async (
|
||||
gameKey: string,
|
||||
title: string
|
||||
): Promise<void> => {
|
||||
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||
if (existingAssets) {
|
||||
const updatedAssets = {
|
||||
...existingAssets,
|
||||
title,
|
||||
};
|
||||
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise<void> => {
|
||||
if (oldAssetPaths.length === 0) return;
|
||||
|
||||
for (const assetPath of oldAssetPaths) {
|
||||
try {
|
||||
if (fs.existsSync(assetPath)) {
|
||||
await fs.promises.unlink(assetPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete old custom asset ${assetPath}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface UpdateGameCustomAssetsParams {
|
||||
shop: GameShop;
|
||||
objectId: string;
|
||||
title: string;
|
||||
customIconUrl?: string | null;
|
||||
customLogoImageUrl?: string | null;
|
||||
customHeroImageUrl?: string | null;
|
||||
customOriginalIconPath?: string | null;
|
||||
customOriginalLogoPath?: string | null;
|
||||
customOriginalHeroPath?: string | null;
|
||||
}
|
||||
|
||||
const updateGameCustomAssets = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
params: UpdateGameCustomAssetsParams
|
||||
) => {
|
||||
const {
|
||||
shop,
|
||||
objectId,
|
||||
title,
|
||||
customIconUrl,
|
||||
customLogoImageUrl,
|
||||
customHeroImageUrl,
|
||||
customOriginalIconPath,
|
||||
customOriginalLogoPath,
|
||||
customOriginalHeroPath,
|
||||
} = params;
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const existingGame = await gamesSublevel.get(gameKey);
|
||||
if (!existingGame) {
|
||||
throw new Error("Game not found");
|
||||
}
|
||||
|
||||
const oldAssetPaths = collectOldAssetPaths(
|
||||
existingGame,
|
||||
customIconUrl,
|
||||
customLogoImageUrl,
|
||||
customHeroImageUrl
|
||||
);
|
||||
|
||||
const updatedGame = await updateGameData({
|
||||
gameKey,
|
||||
existingGame,
|
||||
title,
|
||||
customIconUrl,
|
||||
customLogoImageUrl,
|
||||
customHeroImageUrl,
|
||||
customOriginalIconPath,
|
||||
customOriginalLogoPath,
|
||||
customOriginalHeroPath,
|
||||
});
|
||||
|
||||
await updateShopAssets(gameKey, title);
|
||||
|
||||
await deleteOldAssetFiles(oldAssetPaths);
|
||||
|
||||
return updatedGame;
|
||||
};
|
||||
|
||||
registerEvent("updateGameCustomAssets", updateGameCustomAssets);
|
||||
18
src/main/events/misc/delete-temp-file.ts
Normal file
18
src/main/events/misc/delete-temp-file.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import fs from "node:fs";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const deleteTempFile = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
filePath: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - temp files will be cleaned up by OS eventually
|
||||
console.warn(`Failed to delete temp file: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("deleteTempFile", deleteTempFile);
|
||||
27
src/main/events/misc/save-temp-file.ts
Normal file
27
src/main/events/misc/save-temp-file.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { app } from "electron";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const saveTempFile = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
fileName: string,
|
||||
fileData: Uint8Array
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const tempDir = app.getPath("temp");
|
||||
const tempFilePath = path.join(
|
||||
tempDir,
|
||||
`hydra-temp-${Date.now()}-${fileName}`
|
||||
);
|
||||
|
||||
// Write the file data to temp directory
|
||||
fs.writeFileSync(tempFilePath, fileData);
|
||||
|
||||
return tempFilePath;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save temp file: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("saveTempFile", saveTempFile);
|
||||
@@ -53,6 +53,8 @@ const startGameDownload = async (
|
||||
await gamesSublevel.put(gameKey, {
|
||||
title,
|
||||
iconUrl: gameAssets?.iconUrl ?? null,
|
||||
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
|
||||
logoImageUrl: gameAssets?.logoImageUrl ?? null,
|
||||
objectId,
|
||||
shop,
|
||||
remoteId: null,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { registerEvent } from "../register-event";
|
||||
|
||||
import { HydraApi } from "@main/services";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
|
||||
|
||||
const getComparedUnlockedAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -10,6 +11,8 @@ const getComparedUnlockedAchievements = async (
|
||||
shop: GameShop,
|
||||
userId: string
|
||||
) => {
|
||||
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GameShop, UserAchievement, UserPreferences } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
|
||||
|
||||
export const getUnlockedAchievements = async (
|
||||
objectId: string,
|
||||
@@ -62,7 +63,7 @@ export const getUnlockedAchievements = async (
|
||||
!achievementData.hidden || showHiddenAchievementsDescription
|
||||
? achievementData.description
|
||||
: undefined,
|
||||
} as UserAchievement;
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.unlocked && !b.unlocked) return -1;
|
||||
@@ -79,6 +80,7 @@ const getUnlockedAchievementsEvent = async (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
): Promise<UserAchievement[]> => {
|
||||
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
||||
return getUnlockedAchievements(objectId, shop, false);
|
||||
};
|
||||
|
||||
|
||||
28
src/main/events/user/get-user-library.ts
Normal file
28
src/main/events/user/get-user-library.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { UserLibraryResponse } from "@types";
|
||||
|
||||
const getUserLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string,
|
||||
take: number = 12,
|
||||
skip: number = 0,
|
||||
sortBy?: string
|
||||
): Promise<UserLibraryResponse | null> => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
params.append("take", take.toString());
|
||||
params.append("skip", skip.toString());
|
||||
|
||||
if (sortBy) {
|
||||
params.append("sortBy", sortBy);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const baseUrl = `/users/${userId}/library`;
|
||||
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
|
||||
return HydraApi.get<UserLibraryResponse>(url).catch(() => null);
|
||||
};
|
||||
|
||||
registerEvent("getUserLibrary", getUserLibrary);
|
||||
@@ -4,7 +4,12 @@ import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import {
|
||||
logger,
|
||||
clearGamesPlaytime,
|
||||
WindowManager,
|
||||
Lock,
|
||||
} from "@main/services";
|
||||
import resources from "@locales";
|
||||
import { PythonRPC } from "./services/python-rpc";
|
||||
import { db, levelKeys } from "./level";
|
||||
@@ -59,6 +64,71 @@ app.whenReady().then(async () => {
|
||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||
});
|
||||
|
||||
protocol.handle("gradient", (request) => {
|
||||
const gradientCss = decodeURIComponent(
|
||||
request.url.slice("gradient:".length)
|
||||
);
|
||||
|
||||
// Parse gradient CSS safely without regex to prevent ReDoS
|
||||
let direction = "45deg";
|
||||
let color1 = "#4a90e2";
|
||||
let color2 = "#7b68ee";
|
||||
|
||||
// Simple string parsing approach - more secure than regex
|
||||
if (
|
||||
gradientCss.startsWith("linear-gradient(") &&
|
||||
gradientCss.endsWith(")")
|
||||
) {
|
||||
const content = gradientCss.slice(16, -1); // Remove "linear-gradient(" and ")"
|
||||
const parts = content.split(",").map((part) => part.trim());
|
||||
|
||||
if (parts.length >= 3) {
|
||||
direction = parts[0];
|
||||
color1 = parts[1];
|
||||
color2 = parts[2];
|
||||
}
|
||||
}
|
||||
|
||||
let x1 = "0%",
|
||||
y1 = "0%",
|
||||
x2 = "100%",
|
||||
y2 = "100%";
|
||||
|
||||
if (direction === "to right") {
|
||||
y2 = "0%";
|
||||
} else if (direction === "to bottom") {
|
||||
x2 = "0%";
|
||||
} else if (direction === "45deg") {
|
||||
y1 = "100%";
|
||||
y2 = "0%";
|
||||
} else if (direction === "225deg") {
|
||||
x1 = "100%";
|
||||
x2 = "0%";
|
||||
} else if (direction === "315deg") {
|
||||
x1 = "100%";
|
||||
y1 = "100%";
|
||||
x2 = "0%";
|
||||
y2 = "0%";
|
||||
}
|
||||
// Note: "135deg" case removed as it uses all default values
|
||||
|
||||
const svgContent = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
||||
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grad)" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return new Response(svgContent, {
|
||||
headers: { "Content-Type": "image/svg+xml" },
|
||||
});
|
||||
});
|
||||
|
||||
await loadState();
|
||||
|
||||
const language = await db
|
||||
@@ -143,9 +213,19 @@ app.on("window-all-closed", () => {
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
/* Disconnects libtorrent */
|
||||
PythonRPC.kill();
|
||||
let canAppBeClosed = false;
|
||||
|
||||
app.on("before-quit", async (e) => {
|
||||
await Lock.releaseLock();
|
||||
|
||||
if (!canAppBeClosed) {
|
||||
e.preventDefault();
|
||||
/* Disconnects libtorrent */
|
||||
PythonRPC.kill();
|
||||
await clearGamesPlaytime();
|
||||
canAppBeClosed = true;
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
|
||||
11
src/main/level/sublevels/game-stats-cache.ts
Normal file
11
src/main/level/sublevels/game-stats-cache.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { GameStats } from "@types";
|
||||
|
||||
import { db } from "../level";
|
||||
import { levelKeys } from "./keys";
|
||||
|
||||
export const gamesStatsCacheSublevel = db.sublevel<
|
||||
string,
|
||||
GameStats & { updatedAt: number }
|
||||
>(levelKeys.gameStatsCache, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
@@ -2,6 +2,7 @@ export * from "./downloads";
|
||||
export * from "./games";
|
||||
export * from "./game-shop-assets";
|
||||
export * from "./game-shop-cache";
|
||||
export * from "./game-stats-cache";
|
||||
export * from "./game-achievements";
|
||||
export * from "./keys";
|
||||
export * from "./themes";
|
||||
|
||||
@@ -7,6 +7,7 @@ export const levelKeys = {
|
||||
auth: "auth",
|
||||
themes: "themes",
|
||||
gameShopAssets: "gameShopAssets",
|
||||
gameStatsCache: "gameStatsAssets",
|
||||
gameShopCache: "gameShopCache",
|
||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||
`${shop}:${objectId}:${language}`,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Downloader } from "@shared";
|
||||
import { levelKeys, db } from "./level";
|
||||
import type { UserPreferences } from "@types";
|
||||
import {
|
||||
WSClient,
|
||||
SystemPath,
|
||||
CommonRedistManager,
|
||||
TorBoxClient,
|
||||
@@ -15,9 +14,12 @@ import {
|
||||
uploadGamesBatch,
|
||||
startMainLoop,
|
||||
Ludusavi,
|
||||
Lock,
|
||||
} from "@main/services";
|
||||
|
||||
export const loadState = async () => {
|
||||
await Lock.acquireLock();
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
@@ -40,10 +42,11 @@ export const loadState = async () => {
|
||||
}
|
||||
|
||||
Ludusavi.copyConfigFileToUserData();
|
||||
Ludusavi.copyBinaryToUserData();
|
||||
|
||||
await HydraApi.setupApi().then(() => {
|
||||
uploadGamesBatch();
|
||||
WSClient.connect();
|
||||
// WSClient.connect();
|
||||
});
|
||||
|
||||
const downloads = await downloadsSublevel
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mergeAchievements } from "./merge-achievements";
|
||||
import fs, { readdirSync } from "node:fs";
|
||||
import {
|
||||
findAchievementFileInExecutableDirectory,
|
||||
findAchievementFileInSteamPath,
|
||||
findAchievementFiles,
|
||||
findAllAchievementFiles,
|
||||
getAlternativeObjectIds,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
import type {
|
||||
AchievementFile,
|
||||
Game,
|
||||
GameShop,
|
||||
UnlockedAchievement,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
@@ -18,7 +20,7 @@ import { Cracker } from "@shared";
|
||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { sleep } from "@main/helpers";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
|
||||
const fileStats: Map<string, number> = new Map();
|
||||
const fltFiles: Map<string, Set<string>> = new Map();
|
||||
@@ -37,15 +39,19 @@ const watchAchievementsWindows = async () => {
|
||||
const gameAchievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
|
||||
gameAchievementFiles.push(...(achievementFiles.get(objectId) ?? []));
|
||||
|
||||
gameAchievementFiles.push(
|
||||
...findAchievementFileInExecutableDirectory(game)
|
||||
);
|
||||
|
||||
gameAchievementFiles.push(
|
||||
...(await findAchievementFileInSteamPath(game))
|
||||
);
|
||||
}
|
||||
|
||||
for (const file of gameAchievementFiles) {
|
||||
compareFile(game, file);
|
||||
await compareFile(game, file);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -60,13 +66,11 @@ const watchAchievementsWithWine = async () => {
|
||||
|
||||
for (const game of games) {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||
|
||||
for (const file of gameAchievementFiles) {
|
||||
compareFile(game, file);
|
||||
await compareFile(game, file);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -127,6 +131,11 @@ const compareFile = (game: Game, file: AchievementFile) => {
|
||||
);
|
||||
return processAchievementFileDiff(game, file);
|
||||
} catch (err) {
|
||||
achievementsLogger.error(
|
||||
"Error reading file",
|
||||
file.filePath,
|
||||
err instanceof Error ? err.message : err
|
||||
);
|
||||
fileStats.set(file.filePath, -1);
|
||||
return;
|
||||
}
|
||||
@@ -136,20 +145,66 @@ const processAchievementFileDiff = async (
|
||||
game: Game,
|
||||
file: AchievementFile
|
||||
) => {
|
||||
const unlockedAchievements = parseAchievementFile(file.filePath, file.type);
|
||||
const parsedAchievements = parseAchievementFile(file.filePath, file.type);
|
||||
|
||||
if (unlockedAchievements.length) {
|
||||
return mergeAchievements(game, unlockedAchievements, true);
|
||||
if (parsedAchievements.length) {
|
||||
return mergeAchievements(game, parsedAchievements, true);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export class AchievementWatcherManager {
|
||||
private static hasFinishedMergingWithRemote = false;
|
||||
private static _hasFinishedPreSearch = false;
|
||||
|
||||
public static get hasFinishedPreSearch() {
|
||||
return this._hasFinishedPreSearch;
|
||||
}
|
||||
|
||||
public static readonly alreadySyncedGames: Map<string, boolean> = new Map();
|
||||
|
||||
public static async firstSyncWithRemoteIfNeeded(
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
if (this.alreadySyncedGames.get(gameKey)) return;
|
||||
|
||||
this.alreadySyncedGames.set(gameKey, true);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey).catch(() => null);
|
||||
if (!game || game.isDeleted) return;
|
||||
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
|
||||
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||
|
||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievementFile of gameAchievementFiles) {
|
||||
const localAchievementFile = parseAchievementFile(
|
||||
achievementFile.filePath,
|
||||
achievementFile.type
|
||||
);
|
||||
|
||||
if (localAchievementFile.length) {
|
||||
unlockedAchievements.push(...localAchievementFile);
|
||||
}
|
||||
}
|
||||
|
||||
const newAchievements = await mergeAchievements(
|
||||
game,
|
||||
unlockedAchievements,
|
||||
false
|
||||
);
|
||||
|
||||
if (newAchievements > 0) {
|
||||
this.notifyCombinedAchievementsUnlocked(1, newAchievements);
|
||||
}
|
||||
}
|
||||
|
||||
public static watchAchievements() {
|
||||
if (!this.hasFinishedMergingWithRemote) return;
|
||||
if (!this.hasFinishedPreSearch) return;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
return watchAchievementsWindows();
|
||||
@@ -188,7 +243,11 @@ export class AchievementWatcherManager {
|
||||
}
|
||||
}
|
||||
|
||||
return mergeAchievements(game, unlockedAchievements, false);
|
||||
if (unlockedAchievements.length) {
|
||||
return mergeAchievements(game, unlockedAchievements, false);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async getGameAchievementFilesWindows() {
|
||||
@@ -200,7 +259,7 @@ export class AchievementWatcherManager {
|
||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
games.map(async (game) => {
|
||||
const achievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
@@ -211,6 +270,10 @@ export class AchievementWatcherManager {
|
||||
achievementFiles.push(
|
||||
...findAchievementFileInExecutableDirectory(game)
|
||||
);
|
||||
|
||||
achievementFiles.push(
|
||||
...(await findAchievementFileInSteamPath(game))
|
||||
);
|
||||
}
|
||||
|
||||
return { game, achievementFiles };
|
||||
@@ -225,37 +288,54 @@ export class AchievementWatcherManager {
|
||||
.then((games) => games.filter((game) => !game.isDeleted));
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
games.map(async (game) => {
|
||||
const achievementFiles = findAchievementFiles(game);
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
achievementFiles.push(...achievementFileInsideDirectory);
|
||||
achievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||
|
||||
return { game, achievementFiles };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public static async preSearchAchievements() {
|
||||
await sleep(2000);
|
||||
private static async notifyCombinedAchievementsUnlocked(
|
||||
totalNewGamesWithAchievements: number,
|
||||
totalNewAchievements: number
|
||||
) {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-combined-achievements-unlocked",
|
||||
totalNewGamesWithAchievements,
|
||||
totalNewAchievements,
|
||||
userPreferences.achievementCustomNotificationPosition ?? "top-left"
|
||||
);
|
||||
} else {
|
||||
publishCombinedNewAchievementNotification(
|
||||
totalNewAchievements,
|
||||
totalNewGamesWithAchievements
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async preSearchAchievements() {
|
||||
try {
|
||||
const gameAchievementFiles =
|
||||
process.platform === "win32"
|
||||
? await this.getGameAchievementFilesWindows()
|
||||
: await this.getGameAchievementFilesLinux();
|
||||
|
||||
const newAchievementsCount: number[] = [];
|
||||
|
||||
for (const { game, achievementFiles } of gameAchievementFiles) {
|
||||
const result = await this.preProcessGameAchievementFiles(
|
||||
game,
|
||||
achievementFiles
|
||||
);
|
||||
|
||||
newAchievementsCount.push(result);
|
||||
}
|
||||
const newAchievementsCount = await Promise.all(
|
||||
gameAchievementFiles.map(({ game, achievementFiles }) => {
|
||||
return this.preProcessGameAchievementFiles(game, achievementFiles);
|
||||
})
|
||||
);
|
||||
|
||||
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
||||
(achievements) => achievements
|
||||
@@ -267,34 +347,16 @@ export class AchievementWatcherManager {
|
||||
);
|
||||
|
||||
if (totalNewAchievements > 0) {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
await setTimeout(4000);
|
||||
this.notifyCombinedAchievementsUnlocked(
|
||||
totalNewGamesWithAchievements,
|
||||
totalNewAchievements
|
||||
);
|
||||
|
||||
if (userPreferences.achievementNotificationsEnabled !== false) {
|
||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-combined-achievements-unlocked",
|
||||
totalNewGamesWithAchievements,
|
||||
totalNewAchievements,
|
||||
userPreferences.achievementCustomNotificationPosition ??
|
||||
"top-left"
|
||||
);
|
||||
} else {
|
||||
publishCombinedNewAchievementNotification(
|
||||
totalNewAchievements,
|
||||
totalNewGamesWithAchievements
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
achievementsLogger.error("Error on preSearchAchievements", err);
|
||||
}
|
||||
|
||||
this.hasFinishedMergingWithRemote = true;
|
||||
this._hasFinishedPreSearch = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { Game, AchievementFile } from "@types";
|
||||
import type { Game, AchievementFile, UserPreferences } from "@types";
|
||||
import { Cracker } from "@shared";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { SystemPath } from "../system-path";
|
||||
import { getSteamLocation, getSteamUsersIds } from "../steam";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
const getAppDataPath = () => {
|
||||
if (process.platform === "win32") {
|
||||
@@ -270,6 +272,55 @@ export const findAchievementFiles = (game: Game) => {
|
||||
}
|
||||
}
|
||||
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
return achievementFiles.concat(achievementFileInsideDirectory);
|
||||
};
|
||||
|
||||
const steamUserIds = await getSteamUsersIds();
|
||||
const steamPath = await getSteamLocation().catch(() => null);
|
||||
|
||||
export const findAchievementFileInSteamPath = async (game: Game) => {
|
||||
if (!steamUserIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!steamPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (!userPreferences?.enableSteamAchievements) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const achievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const steamUserId of steamUserIds) {
|
||||
const gameAchievementPath = path.join(
|
||||
steamPath,
|
||||
"userdata",
|
||||
steamUserId.toString(),
|
||||
"config",
|
||||
"librarycache",
|
||||
`${game.objectId}.json`
|
||||
);
|
||||
|
||||
if (fs.existsSync(gameAchievementPath)) {
|
||||
achievementFiles.push({
|
||||
type: Cracker.Steam,
|
||||
filePath: gameAchievementPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return achievementFiles;
|
||||
};
|
||||
|
||||
@@ -303,7 +354,7 @@ export const findAchievementFileInExecutableDirectory = (
|
||||
"achievements.ini"
|
||||
),
|
||||
},
|
||||
];
|
||||
].filter((file) => fs.existsSync(file.filePath)) as AchievementFile[];
|
||||
};
|
||||
|
||||
const mapFileLocationWithObjectId = (
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import type { GameShop, SteamAchievement } from "@types";
|
||||
import type { GameAchievement, GameShop, SteamAchievement } from "@types";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
const getModifiedSinceHeader = (
|
||||
cachedAchievements: GameAchievement | undefined
|
||||
): Date | undefined => {
|
||||
if (!cachedAchievements) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cachedAchievements.updatedAt
|
||||
? new Date(cachedAchievements.updatedAt)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const getGameAchievementData = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
useCachedData: boolean
|
||||
) => {
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
if (cachedAchievements && useCachedData)
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
|
||||
|
||||
if (cachedAchievements?.achievements && useCachedData)
|
||||
return cachedAchievements.achievements;
|
||||
|
||||
if (
|
||||
cachedAchievements &&
|
||||
Date.now() < (cachedAchievements.cacheExpiresTimestamp ?? 0)
|
||||
cachedAchievements?.achievements &&
|
||||
Date.now() < (cachedAchievements.updatedAt ?? 0) + LOCAL_CACHE_EXPIRATION
|
||||
) {
|
||||
return cachedAchievements.achievements;
|
||||
}
|
||||
@@ -29,18 +44,22 @@ export const getGameAchievementData = async (
|
||||
})
|
||||
.then((language) => language || "en");
|
||||
|
||||
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
|
||||
shop,
|
||||
objectId,
|
||||
language,
|
||||
})
|
||||
return HydraApi.get<SteamAchievement[]>(
|
||||
"/games/achievements",
|
||||
{
|
||||
shop,
|
||||
objectId,
|
||||
language,
|
||||
},
|
||||
{
|
||||
ifModifiedSince: getModifiedSinceHeader(cachedAchievements),
|
||||
}
|
||||
)
|
||||
.then(async (achievements) => {
|
||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
await gameAchievementsSublevel.put(gameKey, {
|
||||
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||
achievements,
|
||||
cacheExpiresTimestamp: achievements.length
|
||||
? Date.now() + 1000 * 60 * 30 // 30 minutes
|
||||
: undefined,
|
||||
updatedAt: Date.now() + LOCAL_CACHE_EXPIRATION,
|
||||
});
|
||||
|
||||
return achievements;
|
||||
@@ -50,8 +69,14 @@ export const getGameAchievementData = async (
|
||||
throw err;
|
||||
}
|
||||
|
||||
const isNotModified = (err as AxiosError)?.response?.status === 304;
|
||||
|
||||
if (isNotModified) {
|
||||
return cachedAchievements?.achievements ?? [];
|
||||
}
|
||||
|
||||
logger.error("Failed to get game achievements for", objectId, err);
|
||||
|
||||
return [];
|
||||
return cachedAchievements?.achievements ?? [];
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SubscriptionRequiredError } from "@shared";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { getGameAchievementData } from "./get-game-achievement-data";
|
||||
import { AchievementWatcherManager } from "./achievement-watcher-manager";
|
||||
|
||||
const isRareAchievement = (points: number) => {
|
||||
const rawPercentage = (50 - Math.sqrt(points)) * 2;
|
||||
@@ -35,7 +36,7 @@ const saveAchievementsOnLocal = async (
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
achievements: gameAchievement?.achievements ?? [],
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
cacheExpiresTimestamp: gameAchievement?.cacheExpiresTimestamp,
|
||||
updatedAt: gameAchievement?.updatedAt,
|
||||
});
|
||||
|
||||
if (!sendUpdateEvent) return;
|
||||
@@ -56,9 +57,9 @@ export const mergeAchievements = async (
|
||||
achievements: UnlockedAchievement[],
|
||||
publishNotification: boolean
|
||||
) => {
|
||||
let localGameAchievement = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(game.shop, game.objectId)
|
||||
);
|
||||
const gameKey = levelKeys.game(game.shop, game.objectId);
|
||||
|
||||
let localGameAchievement = await gameAchievementsSublevel.get(gameKey);
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
@@ -67,10 +68,8 @@ export const mergeAchievements = async (
|
||||
);
|
||||
|
||||
if (!localGameAchievement) {
|
||||
await getGameAchievementData(game.objectId, game.shop, true);
|
||||
localGameAchievement = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(game.shop, game.objectId)
|
||||
);
|
||||
await getGameAchievementData(game.objectId, game.shop, false);
|
||||
localGameAchievement = await gameAchievementsSublevel.get(gameKey);
|
||||
}
|
||||
|
||||
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||
@@ -136,6 +135,12 @@ export const mergeAchievements = async (
|
||||
};
|
||||
});
|
||||
|
||||
achievementsLogger.log(
|
||||
"Publishing achievement notification",
|
||||
game.objectId,
|
||||
game.title
|
||||
);
|
||||
|
||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
@@ -153,7 +158,11 @@ export const mergeAchievements = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (game.remoteId) {
|
||||
const shouldSyncWithRemote =
|
||||
game.remoteId &&
|
||||
(newAchievements.length || AchievementWatcherManager.hasFinishedPreSearch);
|
||||
|
||||
if (shouldSyncWithRemote) {
|
||||
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
|
||||
"/profile/games/achievements",
|
||||
{
|
||||
@@ -194,8 +203,11 @@ export const mergeAchievements = async (
|
||||
mergedLocalAchievements,
|
||||
publishNotification
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
AchievementWatcherManager.alreadySyncedGames.set(gameKey, true);
|
||||
});
|
||||
} else {
|
||||
} else if (newAchievements.length) {
|
||||
await saveAchievementsOnLocal(
|
||||
game.objectId,
|
||||
game.shop,
|
||||
|
||||
@@ -75,6 +75,11 @@ export const parseAchievementFile = (
|
||||
return processRazor1911(filePath);
|
||||
}
|
||||
|
||||
if (type === Cracker.Steam) {
|
||||
const parsed = jsonParse(filePath);
|
||||
return processSteamCacheAchievement(parsed);
|
||||
}
|
||||
|
||||
achievementsLogger.log(
|
||||
`Unprocessed ${type} achievements found on ${filePath}`
|
||||
);
|
||||
@@ -234,6 +239,35 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
|
||||
const processSteamCacheAchievement = (
|
||||
unlockedAchievements: any[]
|
||||
): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
const achievementIndex = unlockedAchievements.findIndex(
|
||||
(element) => element[0] === "achievements"
|
||||
);
|
||||
|
||||
if (achievementIndex === -1) {
|
||||
achievementsLogger.info("No achievements found in Steam cache file");
|
||||
return [];
|
||||
}
|
||||
|
||||
const unlockedAchievementsData =
|
||||
unlockedAchievements[achievementIndex][1]["data"]["vecHighlight"];
|
||||
|
||||
for (const achievement of unlockedAchievementsData) {
|
||||
if (achievement.bAchieved) {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement.strID,
|
||||
unlockTime: achievement.rtUnlocked * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newUnlockedAchievements;
|
||||
};
|
||||
|
||||
const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {
|
||||
findAchievementFiles,
|
||||
findAchievementFileInExecutableDirectory,
|
||||
} from "./find-achivement-files";
|
||||
import { parseAchievementFile } from "./parse-achievement-file";
|
||||
import { mergeAchievements } from "./merge-achievements";
|
||||
import type { Game, UnlockedAchievement } from "@types";
|
||||
|
||||
export const updateLocalUnlockedAchievements = async (game: Game) => {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
|
||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
for (const achievementFile of gameAchievementFiles) {
|
||||
const localAchievementFile = parseAchievementFile(
|
||||
achievementFile.filePath,
|
||||
achievementFile.type
|
||||
);
|
||||
|
||||
if (localAchievementFile.length) {
|
||||
unlockedAchievements.push(...localAchievementFile);
|
||||
}
|
||||
}
|
||||
|
||||
mergeAchievements(game, unlockedAchievements, false);
|
||||
};
|
||||
@@ -31,6 +31,8 @@ export interface ProcessPayload {
|
||||
exe: string | null;
|
||||
pid: number;
|
||||
name: string;
|
||||
environ?: Record<string, string> | null;
|
||||
cwd?: string | null;
|
||||
}
|
||||
|
||||
export interface PauseSeedingPayload {
|
||||
|
||||
@@ -11,11 +11,11 @@ import { getUserData } from "./user/get-user-data";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
import type { Auth, User } from "@types";
|
||||
import { WSClient } from "./ws/ws-client";
|
||||
|
||||
interface HydraApiOptions {
|
||||
needsAuth?: boolean;
|
||||
needsSubscription?: boolean;
|
||||
ifModifiedSince?: Date;
|
||||
}
|
||||
|
||||
interface HydraApiUserAuth {
|
||||
@@ -102,8 +102,8 @@ export class HydraApi {
|
||||
WindowManager.mainWindow.webContents.send("on-signin");
|
||||
await clearGamesRemoteIds();
|
||||
uploadGamesBatch();
|
||||
WSClient.close();
|
||||
WSClient.connect();
|
||||
// WSClient.close();
|
||||
// WSClient.connect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,8 +337,13 @@ export class HydraApi {
|
||||
) {
|
||||
await this.validateOptions(options);
|
||||
|
||||
const headers = {
|
||||
...this.getAxiosConfig().headers,
|
||||
"Hydra-If-Modified-Since": options?.ifModifiedSince?.toUTCString(),
|
||||
};
|
||||
|
||||
return this.instance
|
||||
.get<T>(url, { params, ...this.getAxiosConfig() })
|
||||
.get<T>(url, { params, ...this.getAxiosConfig(), headers })
|
||||
.then((response) => response.data)
|
||||
.catch(this.handleUnauthorizedError);
|
||||
}
|
||||
|
||||
@@ -16,3 +16,4 @@ export * from "./ws";
|
||||
export * from "./system-path";
|
||||
export * from "./library-sync";
|
||||
export * from "./wine";
|
||||
export * from "./lock";
|
||||
|
||||
@@ -6,15 +6,17 @@ type ProfileGame = {
|
||||
id: string;
|
||||
lastTimePlayed: Date | null;
|
||||
playTimeInMilliseconds: number;
|
||||
hasManuallyUpdatedPlaytime: boolean;
|
||||
isFavorite?: boolean;
|
||||
isPinned?: boolean;
|
||||
} & ShopAssets;
|
||||
|
||||
export const mergeWithRemoteGames = async () => {
|
||||
return HydraApi.get<ProfileGame[]>("/profile/games")
|
||||
.then(async (response) => {
|
||||
for (const game of response) {
|
||||
const localGame = await gamesSublevel.get(
|
||||
levelKeys.game(game.shop, game.objectId)
|
||||
);
|
||||
const gameKey = levelKeys.game(game.shop, game.objectId);
|
||||
const localGame = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (localGame) {
|
||||
const updatedLastTimePlayed =
|
||||
@@ -29,39 +31,43 @@ export const mergeWithRemoteGames = async () => {
|
||||
? game.playTimeInMilliseconds
|
||||
: localGame.playTimeInMilliseconds;
|
||||
|
||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...localGame,
|
||||
remoteId: game.id,
|
||||
lastTimePlayed: updatedLastTimePlayed,
|
||||
playTimeInMilliseconds: updatedPlayTime,
|
||||
favorite: game.isFavorite ?? localGame.favorite,
|
||||
isPinned: game.isPinned ?? localGame.isPinned,
|
||||
});
|
||||
} else {
|
||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
objectId: game.objectId,
|
||||
title: game.title,
|
||||
remoteId: game.id,
|
||||
shop: game.shop,
|
||||
iconUrl: game.iconUrl,
|
||||
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||
logoImageUrl: game.logoImageUrl,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime,
|
||||
isDeleted: false,
|
||||
favorite: game.isFavorite ?? false,
|
||||
isPinned: game.isPinned ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
await gamesShopAssetsSublevel.put(
|
||||
levelKeys.game(game.shop, game.objectId),
|
||||
{
|
||||
shop: game.shop,
|
||||
objectId: game.objectId,
|
||||
title: game.title,
|
||||
coverImageUrl: game.coverImageUrl,
|
||||
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||
libraryImageUrl: game.libraryImageUrl,
|
||||
logoImageUrl: game.logoImageUrl,
|
||||
iconUrl: game.iconUrl,
|
||||
logoPosition: game.logoPosition,
|
||||
}
|
||||
);
|
||||
await gamesShopAssetsSublevel.put(gameKey, {
|
||||
shop: game.shop,
|
||||
objectId: game.objectId,
|
||||
title: localGame?.title || game.title, // Preserve local title if it exists
|
||||
coverImageUrl: game.coverImageUrl,
|
||||
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||
libraryImageUrl: game.libraryImageUrl,
|
||||
logoImageUrl: game.logoImageUrl,
|
||||
iconUrl: game.iconUrl,
|
||||
logoPosition: game.logoPosition,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
@@ -11,7 +11,8 @@ export const uploadGamesBatch = async () => {
|
||||
.all()
|
||||
.then((results) => {
|
||||
return results.filter(
|
||||
(game) => !game.isDeleted && game.remoteId === null
|
||||
(game) =>
|
||||
!game.isDeleted && game.remoteId === null && game.shop !== "custom"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,6 +27,8 @@ export const uploadGamesBatch = async () => {
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
shop: game.shop,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
isFavorite: game.favorite,
|
||||
isPinned: game.isPinned ?? false,
|
||||
};
|
||||
})
|
||||
).catch(() => {});
|
||||
@@ -33,9 +36,7 @@ export const uploadGamesBatch = async () => {
|
||||
|
||||
await mergeWithRemoteGames();
|
||||
|
||||
if (HydraApi.isLoggedIn()) {
|
||||
AchievementWatcherManager.preSearchAchievements();
|
||||
}
|
||||
AchievementWatcherManager.preSearchAchievements();
|
||||
|
||||
if (WindowManager.mainWindow)
|
||||
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
||||
|
||||
39
src/main/services/lock.ts
Normal file
39
src/main/services/lock.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { SystemPath } from "./system-path";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export class Lock {
|
||||
private static lockFilePath = path.join(
|
||||
SystemPath.getPath("temp"),
|
||||
"hydra-launcher.lock"
|
||||
);
|
||||
|
||||
public static async acquireLock() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.writeFile(this.lockFilePath, "", (err) => {
|
||||
if (err) {
|
||||
logger.error("Error acquiring the lock", err);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
logger.info("Acquired the lock");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static async releaseLock() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.unlink(this.lockFilePath, (err) => {
|
||||
if (err) {
|
||||
logger.error("Error releasing the lock", err);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
logger.info("Released the lock");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,19 +8,22 @@ import cp from "node:child_process";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class Ludusavi {
|
||||
private static ludusaviPath = app.isPackaged
|
||||
private static ludusaviResourcesPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi");
|
||||
|
||||
private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
|
||||
private static configPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
"ludusavi"
|
||||
);
|
||||
private static binaryName =
|
||||
process.platform === "win32" ? "ludusavi.exe" : "ludusavi";
|
||||
|
||||
private static binaryPath = path.join(this.configPath, this.binaryName);
|
||||
|
||||
public static async getConfig() {
|
||||
const config = YAML.parse(
|
||||
fs.readFileSync(path.join(this.ludusaviPath, "config.yaml"), "utf-8")
|
||||
fs.readFileSync(path.join(this.configPath, "config.yaml"), "utf-8")
|
||||
) as LudusaviConfig;
|
||||
|
||||
return config;
|
||||
@@ -29,13 +32,23 @@ export class Ludusavi {
|
||||
public static async copyConfigFileToUserData() {
|
||||
if (!fs.existsSync(this.configPath)) {
|
||||
fs.mkdirSync(this.configPath, { recursive: true });
|
||||
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviPath, "config.yaml"),
|
||||
path.join(this.ludusaviResourcesPath, "config.yaml"),
|
||||
path.join(this.configPath, "config.yaml")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async copyBinaryToUserData() {
|
||||
if (!fs.existsSync(this.binaryPath)) {
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviResourcesPath, this.binaryName),
|
||||
this.binaryPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
|
||||
@@ -3,15 +3,13 @@ import { createGame, updateGamePlaytime } from "./library-sync";
|
||||
import type { Game, GameRunning } from "@types";
|
||||
import { PythonRPC } from "./python-rpc";
|
||||
import axios from "axios";
|
||||
import { exec } from "child_process";
|
||||
import { ProcessPayload } from "./download/types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { CloudSync } from "./cloud-sync";
|
||||
|
||||
const commands = {
|
||||
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
|
||||
findWineExecutables: `lsof -c wine 2>/dev/null | grep '\\.exe$' | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
|
||||
};
|
||||
import { logger } from "./logger";
|
||||
import path from "path";
|
||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||
|
||||
export const gamesPlaytime = new Map<
|
||||
string,
|
||||
@@ -28,11 +26,10 @@ interface GameExecutables {
|
||||
[key: string]: ExecutableInfo[];
|
||||
}
|
||||
|
||||
const TICKS_TO_UPDATE_API = 120;
|
||||
const TICKS_TO_UPDATE_API = (3 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 3 minutes
|
||||
let currentTick = 1;
|
||||
|
||||
const isWindowsPlatform = process.platform === "win32";
|
||||
const isLinuxPlatform = process.platform === "linux";
|
||||
const platform = process.platform;
|
||||
|
||||
const getGameExecutables = async () => {
|
||||
const gameExecutables = (
|
||||
@@ -49,18 +46,20 @@ const getGameExecutables = async () => {
|
||||
Object.keys(gameExecutables).forEach((key) => {
|
||||
gameExecutables[key] = gameExecutables[key]
|
||||
.filter((executable) => {
|
||||
if (isWindowsPlatform) {
|
||||
if (platform === "win32") {
|
||||
return executable.os === "win32";
|
||||
} else if (isLinuxPlatform) {
|
||||
} else if (platform === "linux") {
|
||||
return executable.os === "linux" || executable.os === "win32";
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((executable) => {
|
||||
return {
|
||||
name: isWindowsPlatform
|
||||
? executable.name.replace(/\//g, "\\")
|
||||
: executable.name,
|
||||
name:
|
||||
platform === "win32"
|
||||
? executable.name.replace(/\//g, "\\")
|
||||
: executable.name,
|
||||
os: executable.os,
|
||||
exe: executable.name.slice(executable.name.lastIndexOf("/") + 1),
|
||||
};
|
||||
@@ -72,8 +71,9 @@ const getGameExecutables = async () => {
|
||||
|
||||
const gameExecutables = await getGameExecutables();
|
||||
|
||||
const findGamePathByProcess = (
|
||||
const findGamePathByProcess = async (
|
||||
processMap: Map<string, Set<string>>,
|
||||
winePrefixMap: Map<string, string>,
|
||||
gameId: string
|
||||
) => {
|
||||
const executables = gameExecutables[gameId];
|
||||
@@ -82,32 +82,26 @@ const findGamePathByProcess = (
|
||||
const pathSet = processMap.get(executable.exe);
|
||||
|
||||
if (pathSet) {
|
||||
pathSet.forEach(async (path) => {
|
||||
for (const path of pathSet) {
|
||||
if (path.toLowerCase().endsWith(executable.name)) {
|
||||
const gameKey = levelKeys.game("steam", gameId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
gamesSublevel.put(gameKey, {
|
||||
const updatedGame: Game = {
|
||||
...game,
|
||||
executablePath: path,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLinuxPlatform) {
|
||||
exec(commands.findWineDir, (err, out) => {
|
||||
if (err) return;
|
||||
if (process.platform === "linux" && winePrefixMap.has(path)) {
|
||||
updatedGame.winePrefixPath = winePrefixMap.get(path)!;
|
||||
}
|
||||
|
||||
if (game) {
|
||||
gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
winePrefixPath: out.trim().replace("/drive_c/windows", ""),
|
||||
});
|
||||
}
|
||||
});
|
||||
await gamesSublevel.put(gameKey, updatedGame);
|
||||
logger.info("Set game path", gameKey, path);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -117,50 +111,29 @@ const getSystemProcessMap = async () => {
|
||||
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
|
||||
[];
|
||||
|
||||
const map = new Map<string, Set<string>>();
|
||||
const processMap = new Map<string, Set<string>>();
|
||||
const winePrefixMap = new Map<string, string>();
|
||||
|
||||
processes.forEach((process) => {
|
||||
const key = process.name?.toLowerCase();
|
||||
const value = process.exe;
|
||||
const value =
|
||||
platform === "win32"
|
||||
? process.exe
|
||||
: path.join(process.cwd ?? "", process.name ?? "");
|
||||
|
||||
if (!key || !value) return;
|
||||
|
||||
const currentSet = map.get(key) ?? new Set();
|
||||
map.set(key, currentSet.add(value));
|
||||
const STEAM_COMPAT_DATA_PATH = process.environ?.STEAM_COMPAT_DATA_PATH;
|
||||
|
||||
if (STEAM_COMPAT_DATA_PATH) {
|
||||
winePrefixMap.set(value, STEAM_COMPAT_DATA_PATH);
|
||||
}
|
||||
|
||||
const currentSet = processMap.get(key) ?? new Set();
|
||||
processMap.set(key, currentSet.add(value));
|
||||
});
|
||||
|
||||
if (isLinuxPlatform) {
|
||||
await new Promise((res) => {
|
||||
exec(commands.findWineExecutables, (err, out) => {
|
||||
if (err) {
|
||||
res(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const pathSet = new Set(
|
||||
out
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((path) => path.trim())
|
||||
);
|
||||
|
||||
pathSet.forEach((path) => {
|
||||
if (path.startsWith("/usr")) return;
|
||||
|
||||
const key = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
||||
|
||||
if (!key || !path) return;
|
||||
|
||||
const currentSet = map.get(key) ?? new Set();
|
||||
map.set(key, currentSet.add(path));
|
||||
});
|
||||
|
||||
res(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
return { processMap, winePrefixMap };
|
||||
};
|
||||
|
||||
export const watchProcesses = async () => {
|
||||
@@ -173,19 +146,20 @@ export const watchProcesses = async () => {
|
||||
|
||||
if (!games.length) return;
|
||||
|
||||
const processMap = await getSystemProcessMap();
|
||||
const { processMap, winePrefixMap } = await getSystemProcessMap();
|
||||
|
||||
for (const game of games) {
|
||||
const executablePath = game.executablePath;
|
||||
if (!executablePath) {
|
||||
if (gameExecutables[game.objectId]) {
|
||||
findGamePathByProcess(processMap, game.objectId);
|
||||
await findGamePathByProcess(processMap, winePrefixMap, game.objectId);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const executable = executablePath
|
||||
.slice(executablePath.lastIndexOf(isWindowsPlatform ? "\\" : "/") + 1)
|
||||
.slice(executablePath.lastIndexOf(platform === "win32" ? "\\" : "/") + 1)
|
||||
.toLowerCase();
|
||||
|
||||
const hasProcess = processMap.get(executable)?.has(executablePath);
|
||||
@@ -218,6 +192,11 @@ export const watchProcesses = async () => {
|
||||
function onOpenGame(game: Game) {
|
||||
const now = performance.now();
|
||||
|
||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
|
||||
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
||||
lastTick: now,
|
||||
firstTick: now,
|
||||
@@ -225,7 +204,18 @@ function onOpenGame(game: Game) {
|
||||
});
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
game.unsyncedDeltaPlayTimeInMilliseconds ?? 0,
|
||||
new Date()
|
||||
)
|
||||
.then(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
@@ -250,13 +240,7 @@ function onTickGame(game: Game) {
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
|
||||
@@ -266,22 +250,34 @@ function onTickGame(game: Game) {
|
||||
});
|
||||
|
||||
if (currentTick % TICKS_TO_UPDATE_API === 0) {
|
||||
const deltaToSync =
|
||||
now -
|
||||
gamePlaytime.lastSyncTick +
|
||||
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
|
||||
|
||||
const gamePromise = game.remoteId
|
||||
? updateGamePlaytime(
|
||||
game,
|
||||
now - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
)
|
||||
? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
: createGame(game);
|
||||
|
||||
gamePromise
|
||||
.then(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
||||
...gamePlaytime,
|
||||
lastSyncTick: now,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,12 +288,6 @@ const onCloseGame = (game: Game) => {
|
||||
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
performance.now() - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
).catch(() => {});
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
game.objectId,
|
||||
@@ -306,7 +296,38 @@ const onCloseGame = (game: Game) => {
|
||||
CloudSync.getBackupLabel(true)
|
||||
);
|
||||
}
|
||||
|
||||
const deltaToSync =
|
||||
performance.now() -
|
||||
gamePlaytime.lastSyncTick +
|
||||
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
|
||||
|
||||
return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
.then(() => {
|
||||
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
createGame(game).catch(() => {});
|
||||
return createGame(game).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
export const clearGamesPlaytime = async () => {
|
||||
for (const game of gamesPlaytime.keys()) {
|
||||
const gameData = await gamesSublevel.get(game);
|
||||
|
||||
if (gameData) {
|
||||
await onCloseGame(gameData);
|
||||
}
|
||||
}
|
||||
|
||||
gamesPlaytime.clear();
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export const requestSteam250 = async (path: string) => {
|
||||
const { window } = new JSDOM(response.data);
|
||||
const { document } = window;
|
||||
|
||||
return Array.from(document.querySelectorAll(".appline .title a"))
|
||||
return Array.from(document.querySelectorAll("a[data-title]"))
|
||||
.map(($title) => {
|
||||
const steamGameUrl = ($title as HTMLAnchorElement).href;
|
||||
if (!steamGameUrl) return null;
|
||||
|
||||
@@ -42,6 +42,10 @@ export const getSteamLocation = async () => {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
reject(new Error("SteamPath not found in registry"));
|
||||
}
|
||||
|
||||
resolve(value.value);
|
||||
});
|
||||
});
|
||||
@@ -78,9 +82,19 @@ export const getSteamAppDetails = async (
|
||||
};
|
||||
|
||||
export const getSteamUsersIds = async () => {
|
||||
const userDataPath = await getSteamLocation();
|
||||
const steamLocation = await getSteamLocation().catch(() => null);
|
||||
|
||||
const userIds = fs.readdirSync(path.join(userDataPath, "userdata"), {
|
||||
if (!steamLocation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userDataPath = path.join(steamLocation, "userdata");
|
||||
|
||||
if (!fs.existsSync(userDataPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userIds = fs.readdirSync(userDataPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -181,6 +181,9 @@ export class WindowManager {
|
||||
});
|
||||
|
||||
this.mainWindow.on("close", async () => {
|
||||
const mainWindow = this.mainWindow;
|
||||
this.mainWindow = null;
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
@@ -188,9 +191,11 @@ export class WindowManager {
|
||||
}
|
||||
);
|
||||
|
||||
if (this.mainWindow) {
|
||||
const lastBounds = this.mainWindow.getBounds();
|
||||
const isMaximized = this.mainWindow.isMaximized() ?? false;
|
||||
if (mainWindow) {
|
||||
mainWindow.setProgressBar(-1);
|
||||
|
||||
const lastBounds = mainWindow.getBounds();
|
||||
const isMaximized = mainWindow.isMaximized() ?? false;
|
||||
const screenConfig = isMaximized
|
||||
? {
|
||||
x: undefined,
|
||||
@@ -207,9 +212,6 @@ export class WindowManager {
|
||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
|
||||
this.mainWindow.webContents.setWindowOpenHandler((handler) => {
|
||||
@@ -370,14 +372,11 @@ export class WindowManager {
|
||||
},
|
||||
});
|
||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
||||
// visibleOnFullScreen: true,
|
||||
// });
|
||||
|
||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
this.loadNotificationWindowURL();
|
||||
|
||||
if (isStaging) {
|
||||
if (!app.isPackaged || isStaging) {
|
||||
this.notificationWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
@@ -464,7 +463,7 @@ export class WindowManager {
|
||||
editorWindow.once("ready-to-show", () => {
|
||||
editorWindow.show();
|
||||
this.mainWindow?.webContents.openDevTools();
|
||||
if (isStaging) {
|
||||
if (!app.isPackaged || isStaging) {
|
||||
editorWindow.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
@@ -585,9 +584,9 @@ export class WindowManager {
|
||||
tray.popUpContextMenu(contextMenu);
|
||||
};
|
||||
|
||||
tray.setToolTip("Hydra");
|
||||
tray.setToolTip("Hydra Launcher");
|
||||
|
||||
if (process.platform !== "darwin") {
|
||||
if (process.platform === "win32") {
|
||||
await updateSystemTray();
|
||||
|
||||
tray.addListener("double-click", () => {
|
||||
@@ -598,6 +597,18 @@ export class WindowManager {
|
||||
}
|
||||
});
|
||||
|
||||
tray.addListener("right-click", showContextMenu);
|
||||
} else if (process.platform === "linux") {
|
||||
await updateSystemTray();
|
||||
|
||||
tray.addListener("click", () => {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
} else {
|
||||
this.createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
tray.addListener("right-click", showContextMenu);
|
||||
} else {
|
||||
tray.addListener("click", showContextMenu);
|
||||
|
||||
@@ -34,6 +34,10 @@ export class WSClient {
|
||||
});
|
||||
|
||||
this.ws.on("message", (message) => {
|
||||
if (message.toString() === "PONG") {
|
||||
return;
|
||||
}
|
||||
|
||||
const envelope = Envelope.fromBinary(
|
||||
new Uint8Array(Buffer.from(message.toString()))
|
||||
);
|
||||
@@ -112,7 +116,7 @@ export class WSClient {
|
||||
private static startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.ping();
|
||||
this.ws.send("PING");
|
||||
}
|
||||
}, 15_000);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,52 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
),
|
||||
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
||||
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
||||
addCustomGameToLibrary: (
|
||||
title: string,
|
||||
executablePath: string,
|
||||
iconUrl?: string,
|
||||
logoImageUrl?: string,
|
||||
libraryHeroImageUrl?: string
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"addCustomGameToLibrary",
|
||||
title,
|
||||
executablePath,
|
||||
iconUrl,
|
||||
logoImageUrl,
|
||||
libraryHeroImageUrl
|
||||
),
|
||||
copyCustomGameAsset: (
|
||||
sourcePath: string,
|
||||
assetType: "icon" | "logo" | "hero"
|
||||
) => ipcRenderer.invoke("copyCustomGameAsset", sourcePath, assetType),
|
||||
saveTempFile: (fileName: string, fileData: Uint8Array) =>
|
||||
ipcRenderer.invoke("saveTempFile", fileName, fileData),
|
||||
deleteTempFile: (filePath: string) =>
|
||||
ipcRenderer.invoke("deleteTempFile", filePath),
|
||||
cleanupUnusedAssets: () => ipcRenderer.invoke("cleanupUnusedAssets"),
|
||||
updateCustomGame: (params: {
|
||||
shop: GameShop;
|
||||
objectId: string;
|
||||
title: string;
|
||||
iconUrl?: string;
|
||||
logoImageUrl?: string;
|
||||
libraryHeroImageUrl?: string;
|
||||
originalIconPath?: string;
|
||||
originalLogoPath?: string;
|
||||
originalHeroPath?: string;
|
||||
}) => ipcRenderer.invoke("updateCustomGame", params),
|
||||
updateGameCustomAssets: (params: {
|
||||
shop: GameShop;
|
||||
objectId: string;
|
||||
title: string;
|
||||
customIconUrl?: string | null;
|
||||
customLogoImageUrl?: string | null;
|
||||
customHeroImageUrl?: string | null;
|
||||
customOriginalIconPath?: string | null;
|
||||
customOriginalLogoPath?: string | null;
|
||||
customOriginalHeroPath?: string | null;
|
||||
}) => ipcRenderer.invoke("updateGameCustomAssets", params),
|
||||
createGameShortcut: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -143,6 +189,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
|
||||
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
|
||||
updateLaunchOptions: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -189,6 +237,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||
changeGamePlayTime: (shop: GameShop, objectId: string, playtime: number) =>
|
||||
ipcRenderer.invoke("changeGamePlayTime", shop, objectId, playtime),
|
||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
||||
getDefaultWinePrefixSelectionPath: () =>
|
||||
@@ -234,6 +284,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
downloadOptionTitle: string | null
|
||||
) =>
|
||||
ipcRenderer.invoke("uploadSaveGame", objectId, shop, downloadOptionTitle),
|
||||
toggleArtifactFreeze: (gameArtifactId: string, freeze: boolean) =>
|
||||
ipcRenderer.invoke("toggleArtifactFreeze", gameArtifactId, freeze),
|
||||
renameGameArtifact: (gameArtifactId: string, label: string) =>
|
||||
ipcRenderer.invoke("renameGameArtifact", gameArtifactId, label),
|
||||
downloadGameArtifact: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
@@ -360,6 +414,12 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* User */
|
||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||
getUserLibrary: (
|
||||
userId: string,
|
||||
take?: number,
|
||||
skip?: number,
|
||||
sortBy?: string
|
||||
) => ipcRenderer.invoke("getUserLibrary", userId, take, skip, sortBy),
|
||||
blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
|
||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hydra</title>
|
||||
<title>Hydra Launcher</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self' 'unsafe-inline' * data: local:;"
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
4
src/renderer/src/assets/play-logo.svg
Normal file
4
src/renderer/src/assets/play-logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="white" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 409 B |
@@ -4,9 +4,14 @@
|
||||
color: globals.$muted-color;
|
||||
font-size: 10px;
|
||||
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
||||
border: solid 1px globals.$muted-color;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all ease 0.2s;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,36 @@
|
||||
|
||||
.hero {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
max-height: 280px;
|
||||
border-radius: 4px;
|
||||
height: 180px;
|
||||
min-height: 150px;
|
||||
border-radius: 0;
|
||||
color: #dadbe1;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 0px 15px 0px #000000;
|
||||
cursor: pointer;
|
||||
border: solid 1px globals.$border-color;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
height: 220px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
height: 300px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (min-height: 800px) {
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (max-height: 799px) {
|
||||
height: 300px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
&__media {
|
||||
object-fit: cover;
|
||||
@@ -47,10 +67,42 @@
|
||||
&__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3);
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
|
||||
gap: calc(globals.$spacing-unit);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 3);
|
||||
gap: calc(globals.$spacing-unit * 1.5);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3);
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&__logo {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
width: 120px;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (min-height: 800px) {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (max-height: 799px) {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export function Hero() {
|
||||
width="250px"
|
||||
alt={game.description ?? ""}
|
||||
loading="eager"
|
||||
className="hero__logo"
|
||||
/>
|
||||
<p className="hero__description">{game.description}</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.sidebar-adding-custom-game-modal {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 3);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__image-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__image-preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: globals.$spacing-unit;
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
&__preview-image {
|
||||
max-width: 120px;
|
||||
max-height: 80px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FileDirectoryIcon } from "@primer/octicons-react";
|
||||
|
||||
import { Modal, TextField, Button } from "@renderer/components";
|
||||
import { useLibrary, useToast } from "@renderer/hooks";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
generateRandomGradient,
|
||||
} from "@renderer/helpers";
|
||||
|
||||
import "./sidebar-adding-custom-game-modal.scss";
|
||||
|
||||
export interface SidebarAddingCustomGameModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SidebarAddingCustomGameModal({
|
||||
visible,
|
||||
onClose,
|
||||
}: Readonly<SidebarAddingCustomGameModalProps>) {
|
||||
const { t } = useTranslation("sidebar");
|
||||
const { updateLibrary } = useLibrary();
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [gameName, setGameName] = useState("");
|
||||
const [executablePath, setExecutablePath] = useState("");
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
|
||||
const handleSelectExecutable = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{
|
||||
name: t("custom_game_modal_executable"),
|
||||
extensions: ["exe", "msi", "app", "deb", "rpm", "dmg"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const selectedPath = filePaths[0];
|
||||
setExecutablePath(selectedPath);
|
||||
|
||||
if (!gameName.trim()) {
|
||||
const fileName = selectedPath.split(/[\\/]/).pop() || "";
|
||||
const gameNameFromFile = fileName.replace(/\.[^/.]+$/, "");
|
||||
setGameName(gameNameFromFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleGameNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setGameName(event.target.value);
|
||||
};
|
||||
|
||||
const handleAddGame = async () => {
|
||||
if (!gameName.trim() || !executablePath.trim()) {
|
||||
showErrorToast(t("custom_game_modal_fill_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAdding(true);
|
||||
|
||||
try {
|
||||
// Generate gradient URL only for hero image
|
||||
const gameNameForSeed = gameName.trim();
|
||||
const iconUrl = ""; // Don't use gradient for icon
|
||||
const logoImageUrl = ""; // Don't use gradient for logo
|
||||
const libraryHeroImageUrl = generateRandomGradient(); // Only use gradient for hero
|
||||
|
||||
const newGame = await window.electron.addCustomGameToLibrary(
|
||||
gameNameForSeed,
|
||||
executablePath,
|
||||
iconUrl,
|
||||
logoImageUrl,
|
||||
libraryHeroImageUrl
|
||||
);
|
||||
|
||||
showSuccessToast(t("custom_game_modal_success"));
|
||||
updateLibrary();
|
||||
|
||||
const gameDetailsPath = buildGameDetailsPath({
|
||||
shop: "custom",
|
||||
objectId: newGame.objectId,
|
||||
title: newGame.title,
|
||||
});
|
||||
|
||||
navigate(gameDetailsPath);
|
||||
|
||||
setGameName("");
|
||||
setExecutablePath("");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to add custom game:", error);
|
||||
showErrorToast(
|
||||
error instanceof Error ? error.message : t("custom_game_modal_failed")
|
||||
);
|
||||
} finally {
|
||||
setIsAdding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isAdding) {
|
||||
setGameName("");
|
||||
setExecutablePath("");
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = gameName.trim() && executablePath.trim();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("custom_game_modal")}
|
||||
description={t("custom_game_modal_description")}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="sidebar-adding-custom-game-modal__container">
|
||||
<div className="sidebar-adding-custom-game-modal__form">
|
||||
<TextField
|
||||
label={t("custom_game_modal_executable_path")}
|
||||
placeholder={t("custom_game_modal_select_executable")}
|
||||
value={executablePath}
|
||||
readOnly
|
||||
theme="dark"
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleSelectExecutable}
|
||||
disabled={isAdding}
|
||||
>
|
||||
<FileDirectoryIcon />
|
||||
{t("custom_game_modal_browse")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={t("custom_game_modal_title")}
|
||||
placeholder={t("custom_game_modal_enter_title")}
|
||||
value={gameName}
|
||||
onChange={handleGameNameChange}
|
||||
theme="dark"
|
||||
disabled={isAdding}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-adding-custom-game-modal__actions">
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isAdding}
|
||||
>
|
||||
{t("custom_game_modal_cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
theme="primary"
|
||||
onClick={handleAddGame}
|
||||
disabled={!isFormValid || isAdding}
|
||||
>
|
||||
{isAdding
|
||||
? t("custom_game_modal_adding")
|
||||
: t("custom_game_modal_add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import PlayLogo from "@renderer/assets/play-logo.svg?react";
|
||||
import { LibraryGame } from "@types";
|
||||
import cn from "classnames";
|
||||
import { useLocation } from "react-router-dom";
|
||||
@@ -16,6 +17,19 @@ export function SidebarGameItem({
|
||||
}: Readonly<SidebarGameItemProps>) {
|
||||
const location = useLocation();
|
||||
|
||||
const isCustomGame = game.shop === "custom";
|
||||
const sidebarIcon = isCustomGame
|
||||
? game.libraryImageUrl || game.iconUrl
|
||||
: game.customIconUrl || game.iconUrl;
|
||||
|
||||
// Determine fallback icon based on game type
|
||||
const getFallbackIcon = () => {
|
||||
if (isCustomGame) {
|
||||
return <PlayLogo className="sidebar__game-icon" />;
|
||||
}
|
||||
return <SteamLogo className="sidebar__game-icon" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
key={game.id}
|
||||
@@ -30,15 +44,15 @@ export function SidebarGameItem({
|
||||
className="sidebar__menu-item-button"
|
||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
{sidebarIcon ? (
|
||||
<img
|
||||
className="sidebar__game-icon"
|
||||
src={game.iconUrl}
|
||||
src={sidebarIcon}
|
||||
alt={game.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className="sidebar__game-icon" />
|
||||
getFallbackIcon()
|
||||
)}
|
||||
|
||||
<span className="sidebar__menu-item-button-label">
|
||||
|
||||
@@ -98,6 +98,12 @@
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
&__section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
@@ -133,7 +139,11 @@
|
||||
}
|
||||
|
||||
&__help-button-icon {
|
||||
background: linear-gradient(0deg, #16b195 50%, #3e62c0 100%);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
globals.$brand-teal 50%,
|
||||
globals.$brand-blue 100%
|
||||
);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
@@ -142,4 +152,44 @@
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__play-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: globals.$muted-color;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
&:active {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: globals.$brand-teal;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: globals.$muted-color;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
import type { LibraryGame } from "@types";
|
||||
|
||||
@@ -21,8 +22,13 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import cn from "classnames";
|
||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
CommentDiscussionIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { SidebarGameItem } from "./sidebar-game-item";
|
||||
import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal";
|
||||
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
@@ -32,6 +38,8 @@ const SIDEBAR_MAX_WIDTH = 450;
|
||||
|
||||
const initialSidebarWidth = window.localStorage.getItem("sidebarWidth");
|
||||
|
||||
const isGamePlayable = (game: LibraryGame) => Boolean(game.executablePath);
|
||||
|
||||
export function Sidebar() {
|
||||
const filterRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -60,6 +68,21 @@ export function Sidebar() {
|
||||
|
||||
const { showWarningToast } = useToast();
|
||||
|
||||
const [showPlayableOnly, setShowPlayableOnly] = useState(false);
|
||||
const [showAddGameModal, setShowAddGameModal] = useState(false);
|
||||
|
||||
const handlePlayButtonClick = () => {
|
||||
setShowPlayableOnly(!showPlayableOnly);
|
||||
};
|
||||
|
||||
const handleAddGameButtonClick = () => {
|
||||
setShowAddGameModal(true);
|
||||
};
|
||||
|
||||
const handleCloseAddGameModal = () => {
|
||||
setShowAddGameModal(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateLibrary();
|
||||
}, [lastPacket?.gameId, updateLibrary]);
|
||||
@@ -242,7 +265,37 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
<section className="sidebar__section">
|
||||
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||
<div className="sidebar__section-header">
|
||||
<small className="sidebar__section-title">
|
||||
{t("my_library")}
|
||||
</small>
|
||||
<div
|
||||
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar__add-button"
|
||||
onClick={handleAddGameButtonClick}
|
||||
data-tooltip-id="add-custom-game-tooltip"
|
||||
data-tooltip-content={t("add_custom_game_tooltip")}
|
||||
data-tooltip-place="top"
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("sidebar__play-button", {
|
||||
"sidebar__play-button--active": showPlayableOnly,
|
||||
})}
|
||||
onClick={handlePlayButtonClick}
|
||||
data-tooltip-id="show-playable-only-tooltip"
|
||||
data-tooltip-content={t("show_playable_only_tooltip")}
|
||||
data-tooltip-place="top"
|
||||
>
|
||||
<PlayIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
ref={filterRef}
|
||||
@@ -254,6 +307,7 @@ export function Sidebar() {
|
||||
<ul className="sidebar__menu">
|
||||
{filteredLibrary
|
||||
.filter((game) => !game.favorite)
|
||||
.filter((game) => !showPlayableOnly || isGamePlayable(game))
|
||||
.map((game) => (
|
||||
<SidebarGameItem
|
||||
key={game.id}
|
||||
@@ -285,6 +339,14 @@ export function Sidebar() {
|
||||
className="sidebar__handle"
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
|
||||
<SidebarAddingCustomGameModal
|
||||
visible={showAddGameModal}
|
||||
onClose={handleCloseAddGameModal}
|
||||
/>
|
||||
|
||||
<Tooltip id="add-custom-game-tooltip" />
|
||||
<Tooltip id="show-playable-only-tooltip" />
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,6 @@
|
||||
}
|
||||
|
||||
&__error-label {
|
||||
color: globals.$danger-color;
|
||||
color: globals.$error-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,14 @@ export interface CloudSyncContext {
|
||||
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
getGameBackupPreview: () => Promise<void>;
|
||||
getGameArtifacts: () => Promise<void>;
|
||||
toggleArtifactFreeze: (
|
||||
gameArtifactId: string,
|
||||
freeze: boolean
|
||||
) => Promise<void>;
|
||||
restoringBackup: boolean;
|
||||
uploadingBackup: boolean;
|
||||
loadingPreview: boolean;
|
||||
freezingArtifact: boolean;
|
||||
}
|
||||
|
||||
export const cloudSyncContext = createContext<CloudSyncContext>({
|
||||
@@ -47,10 +52,12 @@ export const cloudSyncContext = createContext<CloudSyncContext>({
|
||||
showCloudSyncFilesModal: false,
|
||||
setShowCloudSyncFilesModal: () => {},
|
||||
getGameBackupPreview: async () => {},
|
||||
toggleArtifactFreeze: async () => {},
|
||||
getGameArtifacts: async () => {},
|
||||
restoringBackup: false,
|
||||
uploadingBackup: false,
|
||||
loadingPreview: false,
|
||||
freezingArtifact: false,
|
||||
});
|
||||
|
||||
const { Provider } = cloudSyncContext;
|
||||
@@ -78,6 +85,7 @@ export function CloudSyncContextProvider({
|
||||
const [uploadingBackup, setUploadingBackup] = useState(false);
|
||||
const [showCloudSyncFilesModal, setShowCloudSyncFilesModal] = useState(false);
|
||||
const [loadingPreview, setLoadingPreview] = useState(false);
|
||||
const [freezingArtifact, setFreezingArtifact] = useState(false);
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
@@ -119,6 +127,22 @@ export function CloudSyncContextProvider({
|
||||
[objectId, shop]
|
||||
);
|
||||
|
||||
const toggleArtifactFreeze = useCallback(
|
||||
async (gameArtifactId: string, freeze: boolean) => {
|
||||
setFreezingArtifact(true);
|
||||
try {
|
||||
await window.electron.toggleArtifactFreeze(gameArtifactId, freeze);
|
||||
getGameArtifacts();
|
||||
} catch (err) {
|
||||
logger.error("Failed to toggle artifact freeze", objectId, shop, err);
|
||||
throw err;
|
||||
} finally {
|
||||
setFreezingArtifact(false);
|
||||
}
|
||||
},
|
||||
[objectId, shop, getGameArtifacts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const removeUploadCompleteListener = window.electron.onUploadComplete(
|
||||
objectId,
|
||||
@@ -192,6 +216,7 @@ export function CloudSyncContextProvider({
|
||||
uploadingBackup,
|
||||
showCloudSyncFilesModal,
|
||||
loadingPreview,
|
||||
freezingArtifact,
|
||||
setShowCloudSyncModal,
|
||||
uploadSaveGame,
|
||||
downloadGameArtifact,
|
||||
@@ -199,6 +224,7 @@ export function CloudSyncContextProvider({
|
||||
setShowCloudSyncFilesModal,
|
||||
getGameBackupPreview,
|
||||
getGameArtifacts,
|
||||
toggleArtifactFreeze,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -38,14 +38,12 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
isGameRunning: false,
|
||||
isLoading: false,
|
||||
objectId: undefined,
|
||||
gameColor: "",
|
||||
showRepacksModal: false,
|
||||
showGameOptionsModal: false,
|
||||
stats: null,
|
||||
achievements: null,
|
||||
hasNSFWContentBlocked: false,
|
||||
lastDownloadedOption: null,
|
||||
setGameColor: () => {},
|
||||
selectGameExecutable: async () => null,
|
||||
updateGame: async () => {},
|
||||
setShowGameOptionsModal: () => {},
|
||||
@@ -82,7 +80,6 @@ export function GameDetailsContextProvider({
|
||||
const [stats, setStats] = useState<GameStats | null>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [gameColor, setGameColor] = useState("");
|
||||
const [isGameRunning, setIsGameRunning] = useState(false);
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
||||
@@ -108,7 +105,7 @@ export function GameDetailsContextProvider({
|
||||
return window.electron
|
||||
.getGameByObjectId(shop, objectId)
|
||||
.then((result) => setGame(result));
|
||||
}, [setGame, shop, objectId]);
|
||||
}, [shop, objectId]);
|
||||
|
||||
const isGameDownloading =
|
||||
lastPacket?.gameId === game?.id && game?.download?.status === "active";
|
||||
@@ -182,12 +179,9 @@ export function GameDetailsContextProvider({
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
updateGame();
|
||||
}, [
|
||||
updateGame,
|
||||
dispatch,
|
||||
gameTitle,
|
||||
objectId,
|
||||
shop,
|
||||
i18n.language,
|
||||
@@ -204,6 +198,12 @@ export function GameDetailsContextProvider({
|
||||
dispatch(setHeaderTitle(gameTitle));
|
||||
}, [objectId, gameTitle, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (game?.title) {
|
||||
dispatch(setHeaderTitle(game.title));
|
||||
}
|
||||
}, [game?.title, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onGamesRunning((gamesIds) => {
|
||||
const updatedIsGameRunning =
|
||||
@@ -289,7 +289,6 @@ export function GameDetailsContextProvider({
|
||||
isGameRunning,
|
||||
isLoading,
|
||||
objectId,
|
||||
gameColor,
|
||||
showGameOptionsModal,
|
||||
showRepacksModal,
|
||||
stats,
|
||||
@@ -297,7 +296,6 @@ export function GameDetailsContextProvider({
|
||||
hasNSFWContentBlocked,
|
||||
lastDownloadedOption,
|
||||
setHasNSFWContentBlocked,
|
||||
setGameColor,
|
||||
selectGameExecutable,
|
||||
updateGame,
|
||||
setShowRepacksModal,
|
||||
|
||||
@@ -16,14 +16,12 @@ export interface GameDetailsContext {
|
||||
isGameRunning: boolean;
|
||||
isLoading: boolean;
|
||||
objectId: string | undefined;
|
||||
gameColor: string;
|
||||
showRepacksModal: boolean;
|
||||
showGameOptionsModal: boolean;
|
||||
stats: GameStats | null;
|
||||
achievements: UserAchievement[] | null;
|
||||
hasNSFWContentBlocked: boolean;
|
||||
lastDownloadedOption: GameRepack | null;
|
||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectGameExecutable: () => Promise<string | null>;
|
||||
updateGame: () => Promise<void>;
|
||||
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user