mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
239 Commits
v3.4.10
...
feat/pinni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa152385b1 | ||
|
|
b6be03cea3 | ||
|
|
f027f05e02 | ||
|
|
56391837cd | ||
|
|
31bd523038 | ||
|
|
fd1f13225b | ||
|
|
dba4b2c4be | ||
|
|
05f10fd80b | ||
|
|
d59315e322 | ||
|
|
a29f2ba741 | ||
|
|
d33b0099a1 | ||
|
|
cad50649aa | ||
|
|
b3148855bc | ||
|
|
f56a3ea126 | ||
|
|
eea701f046 | ||
|
|
355d38c0a2 | ||
|
|
366da7839f | ||
|
|
a869902cfb | ||
|
|
f9d51ed33d | ||
|
|
092af7e421 | ||
|
|
33c15baf0e | ||
|
|
a7e4e21167 | ||
|
|
b22e082781 | ||
|
|
c056feb26f | ||
|
|
240b0705d5 | ||
|
|
2604dfea22 | ||
|
|
0adea20565 | ||
|
|
f182c7c8e9 | ||
|
|
d9379fbcb9 | ||
|
|
adc4af731e | ||
|
|
af1b3d4535 | ||
|
|
291935a1bc | ||
|
|
665365abdc | ||
|
|
86da92aa3f | ||
|
|
6ff694c078 | ||
|
|
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 | ||
|
|
7bb7d2e388 | ||
|
|
b1fc9073d6 | ||
|
|
a1a86c7045 | ||
|
|
81cecfe558 | ||
|
|
9a0e3bfc65 | ||
|
|
f7b88b6d31 | ||
|
|
a996519bd8 | ||
|
|
e85d08422e | ||
|
|
73de69b5a6 | ||
|
|
9172098027 | ||
|
|
f19391200c | ||
|
|
5e51877660 | ||
|
|
6bc2d83ffb | ||
|
|
75ac9e8281 | ||
|
|
650b02e673 | ||
|
|
93929ae15f | ||
|
|
95eecb7161 | ||
|
|
0b83554565 | ||
|
|
4485f62946 | ||
|
|
42c3671965 | ||
|
|
a5aabe0ad7 | ||
|
|
276c098fbc | ||
|
|
3455812a43 | ||
|
|
87a994f0f0 | ||
|
|
15ddc71445 | ||
|
|
ee916b998a | ||
|
|
914942d328 | ||
|
|
5ae67a3dc7 | ||
|
|
5475708b36 | ||
|
|
c85f46844e | ||
|
|
1247a105a0 | ||
|
|
3cc4ee3ee4 | ||
|
|
7fca31338c | ||
|
|
0d747d03ab | ||
|
|
6a59036e21 | ||
|
|
baddd4a99b | ||
|
|
c40d26ef0a | ||
|
|
e4f7747200 | ||
|
|
bc06ae5c03 | ||
|
|
39c073634c | ||
|
|
c5beeb861e | ||
|
|
0a4bdf160c | ||
|
|
6f43da8d28 | ||
|
|
42e8a68c08 | ||
|
|
f960bb4f6f | ||
|
|
7f988c0bba | ||
|
|
dcf05d3386 | ||
|
|
96385d90d8 | ||
|
|
96cfa8c015 | ||
|
|
ae067efd5e | ||
|
|
8c16779052 | ||
|
|
5c7a289299 | ||
|
|
e8e524182a | ||
|
|
521d9faa0c | ||
|
|
ca7ac73836 | ||
|
|
ed42935e7b | ||
|
|
f0c5ec6f1a | ||
|
|
66ced3c779 | ||
|
|
4f8212f8e3 | ||
|
|
86de5aa89e | ||
|
|
00065ab0c9 | ||
|
|
e89202f750 | ||
|
|
1df2353f06 | ||
|
|
475ab4119b | ||
|
|
1346ff49a5 | ||
|
|
4ff0132d53 | ||
|
|
749a88b2b6 | ||
|
|
427b77c597 | ||
|
|
e901df9ac7 | ||
|
|
43e565bcc9 | ||
|
|
f4e710c7d1 | ||
|
|
592ac45740 |
@@ -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=
|
||||
|
||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -99,3 +99,4 @@ jobs:
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
dist/*.pacman
|
||||
dist/*.AppImage
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,7 +7,8 @@ out
|
||||
*.log*
|
||||
.env
|
||||
.vite
|
||||
ludusavi/
|
||||
ludusavi/**
|
||||
!ludusavi/config.yaml
|
||||
hydra-python-rpc/
|
||||
.python-version
|
||||
|
||||
|
||||
BIN
binaries/aria2c
Executable file
BIN
binaries/aria2c
Executable file
Binary file not shown.
BIN
binaries/aria2c.exe
Executable file
BIN
binaries/aria2c.exe
Executable file
Binary file not shown.
@@ -3,7 +3,6 @@ productName: Hydra
|
||||
directories:
|
||||
buildResources: build
|
||||
extraResources:
|
||||
- aria2
|
||||
- ludusavi
|
||||
- hydra-python-rpc
|
||||
- seeds
|
||||
@@ -21,6 +20,7 @@ asarUnpack:
|
||||
win:
|
||||
executableName: Hydra
|
||||
extraResources:
|
||||
- from: binaries/aria2c.exe
|
||||
- from: binaries/7z.exe
|
||||
- from: binaries/7z.dll
|
||||
target:
|
||||
@@ -51,6 +51,7 @@ dmg:
|
||||
linux:
|
||||
extraResources:
|
||||
- from: binaries/7zzs
|
||||
- from: binaries/aria2c
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
6
ludusavi/config.yaml
Normal file
6
ludusavi/config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
manifest:
|
||||
enable: false
|
||||
secondary:
|
||||
- url: https://cdn.losbroxas.org/manifest.yaml
|
||||
enable: true
|
||||
customGames: []
|
||||
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.4.10",
|
||||
"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",
|
||||
@@ -49,32 +47,36 @@
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
"color.js": "^1.2.0",
|
||||
"crc": "^4.3.2",
|
||||
"create-desktop-shortcuts": "^1.11.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dexie": "^4.0.10",
|
||||
"diskusage": "^1.2.0",
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-type": "^19.6.0",
|
||||
"file-type": "^20.5.0",
|
||||
"framer-motion": "^12.15.0",
|
||||
"i18next": "^23.11.2",
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
"piscina": "^4.7.0",
|
||||
"rc-virtual-list": "^3.16.1",
|
||||
"parse-torrent": "^11.0.18",
|
||||
"rc-virtual-list": "^3.18.3",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-redux": "^9.1.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-shadow": "^20.6.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"sound-play": "^1.1.0",
|
||||
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tar": "^7.4.3",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"user-agents": "^1.1.387",
|
||||
"winreg": "^1.2.5",
|
||||
"ws": "^8.18.1",
|
||||
"yaml": "^2.6.1",
|
||||
"yup": "^1.5.0",
|
||||
@@ -100,11 +102,12 @@
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/sound-play": "^1.1.3",
|
||||
"@types/user-agents": "^1.0.4",
|
||||
"@types/winreg": "^1.2.36",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"electron": "^31.7.7",
|
||||
"electron": "^32.3.3",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^2.3.0",
|
||||
"electron-vite": "^3.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
@@ -118,5 +121,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"])
|
||||
|
||||
@@ -3,7 +3,6 @@ const tar = require("tar");
|
||||
const util = require("node:util");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const exec = util.promisify(require("node:child_process").exec);
|
||||
|
||||
@@ -15,8 +14,18 @@ const fileName = {
|
||||
darwin: `ludusavi-v${ludusaviVersion}-mac.tar.gz`,
|
||||
};
|
||||
|
||||
const ludusaviBinaryName = {
|
||||
win32: "ludusavi.exe",
|
||||
linux: "ludusavi",
|
||||
darwin: "ludusavi",
|
||||
};
|
||||
|
||||
const downloadLudusavi = async () => {
|
||||
if (fs.existsSync("ludusavi")) {
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(process.cwd(), "ludusavi", ludusaviBinaryName[process.platform])
|
||||
)
|
||||
) {
|
||||
console.log("Ludusavi already exists, skipping download...");
|
||||
return;
|
||||
}
|
||||
@@ -58,79 +67,4 @@ const downloadLudusavi = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const downloadAria2WindowsAndLinux = async () => {
|
||||
const file =
|
||||
process.platform === "win32"
|
||||
? "aria2-1.37.0-win-64bit-build1.zip"
|
||||
: "aria2-1.37.0-1-x86_64.pkg.tar.zst";
|
||||
|
||||
const downloadUrl =
|
||||
process.platform === "win32"
|
||||
? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
|
||||
: "https://archlinux.org/packages/extra/x86_64/aria2/download/";
|
||||
|
||||
console.log(`Downloading ${file}...`);
|
||||
|
||||
const response = await axios.get(downloadUrl, { responseType: "stream" });
|
||||
|
||||
const stream = response.data.pipe(fs.createWriteStream(file));
|
||||
|
||||
stream.on("finish", async () => {
|
||||
console.log(`Downloaded ${file}, extracting...`);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
await exec(`npx extract-zip ${file}`);
|
||||
console.log("Extracted. Renaming folder...");
|
||||
|
||||
fs.mkdirSync("aria2");
|
||||
fs.copyFileSync(
|
||||
path.join(file.replace(".zip", ""), "aria2c.exe"),
|
||||
"aria2/aria2c.exe"
|
||||
);
|
||||
fs.rmSync(file.replace(".zip", ""), { recursive: true });
|
||||
} else {
|
||||
await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
|
||||
console.log("Extracted. Copying binary file...");
|
||||
fs.mkdirSync("aria2");
|
||||
fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
|
||||
fs.rmSync("usr", { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Extracted ${file}, removing compressed downloaded file...`);
|
||||
fs.rmSync(file);
|
||||
});
|
||||
};
|
||||
|
||||
const copyAria2Macos = async () => {
|
||||
console.log("Checking if aria2 is installed...");
|
||||
|
||||
const isAria2Installed = spawnSync("which", ["aria2c"]).status;
|
||||
|
||||
if (isAria2Installed != 0) {
|
||||
console.log("Please install aria2");
|
||||
console.log("brew install aria2");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Copying aria2 binary...");
|
||||
fs.mkdirSync("aria2");
|
||||
await exec(`cp $(which aria2c) aria2/aria2c`);
|
||||
};
|
||||
|
||||
const copyAria2 = () => {
|
||||
const aria2Path =
|
||||
process.platform === "win32" ? "aria2/aria2c.exe" : "aria2/aria2c";
|
||||
|
||||
if (fs.existsSync(aria2Path)) {
|
||||
console.log("Aria2 already exists, skipping download...");
|
||||
return;
|
||||
}
|
||||
if (process.platform == "darwin") {
|
||||
copyAria2Macos();
|
||||
} else {
|
||||
downloadAria2WindowsAndLinux();
|
||||
}
|
||||
};
|
||||
|
||||
copyAria2();
|
||||
downloadLudusavi();
|
||||
|
||||
@@ -20,7 +20,7 @@ const s3 = new S3Client({
|
||||
|
||||
const dist = path.resolve(__dirname, "..", "dist");
|
||||
|
||||
const extensionsToUpload = [".deb", ".exe", ".pacman"];
|
||||
const extensionsToUpload = [".deb", ".exe", ".pacman", ".AppImage"];
|
||||
|
||||
fs.readdir(dist, async (err, files) => {
|
||||
if (err) throw err;
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
{
|
||||
"language_name": "Български",
|
||||
"app": {
|
||||
"successfully_signed_in": "Успешно вписване"
|
||||
"successfully_signed_in": "Успешно влизане"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Препоръчани",
|
||||
"surprise_me": "Изненадай ме",
|
||||
"no_results": "Не са намерени резултати",
|
||||
"start_typing": "Търсене...",
|
||||
"hot": "Актуално сега",
|
||||
"weekly": "📅 Най-доброто от седмицата",
|
||||
"achievements": "🏆 Игри, които да победите"
|
||||
"no_results": "Няма намерени резултати",
|
||||
"start_typing": "Започнете да пишете за търсене...",
|
||||
"hot": "Горещи сега",
|
||||
"weekly": "📅 Топ игри на седмицата",
|
||||
"achievements": "🏆 Игри които да победите"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Каталог",
|
||||
"downloads": "Изтегляния",
|
||||
"settings": "Настройки",
|
||||
"my_library": "Моята библиотека",
|
||||
"downloading_metadata": "{{title}} (Сваляне на метаданни…)",
|
||||
"paused": "{{title}} (Пауза)",
|
||||
"downloading_metadata": "{{title}} (Изтегляне на метаданни…)",
|
||||
"paused": "{{title}} (На пауза)",
|
||||
"downloading": "{{title}} ({{percentage}} - Изтегляне…)",
|
||||
"filter": "Търсене по име",
|
||||
"filter": "Филтрирай библиотеката",
|
||||
"home": "Начало",
|
||||
"queued": "{{title}} (Опашка)",
|
||||
"game_has_no_executable": "Играта няма избран изпълним файл",
|
||||
"sign_in": "Вписване",
|
||||
"queued": "{{title}} (В опашката)",
|
||||
"game_has_no_executable": "Няма избран изпълним файл за играта",
|
||||
"sign_in": "Вход",
|
||||
"friends": "Приятели",
|
||||
"need_help": "Имате нужда от помощ??",
|
||||
"favorites": "Любими игри"
|
||||
"need_help": "Нужда от помощ?",
|
||||
"favorites": "Любими"
|
||||
},
|
||||
"header": {
|
||||
"search": "Търсене",
|
||||
"search": "Търси игри",
|
||||
"home": "Начало",
|
||||
"catalogue": "Каталог",
|
||||
"downloads": "Изтегляния",
|
||||
"search_results": "Резултати от търсене",
|
||||
"search_results": "Резултати от търсенето",
|
||||
"settings": "Настройки",
|
||||
"version_available_install": "Версия {{version}} е налична. Кликни тук, за да рестартирате и инсталирате.",
|
||||
"version_available_download": "Версия {{version}} е налична. Кликни тук за изтегляне."
|
||||
"version_available_install": "Версия {{version}} е налична. Кликнете тук за рестарт и инсталация.",
|
||||
"version_available_download": "Версия {{version}} е налична. Кликнете тук за изтегляне."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Няма изтегляния в ход",
|
||||
"downloading_metadata": "Сваляне на {{title}} метадата…",
|
||||
"downloading": "Изтегляне на {{title}}… ({{percentage}} готово) - Остават {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Изтегляне на {{title}}… ({{percentage}} готово) - Изчисляване на оставащо време…",
|
||||
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
|
||||
"no_downloads_in_progress": "Няма текущи изтегляния",
|
||||
"downloading_metadata": "Изтегляне на метаданни за {{title}}…",
|
||||
"downloading": "Изтегля се {{title}}… ({{percentage}} завършено) - Завършване {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Изтегля се {{title}}… ({{percentage}} завършено) - Изчисляване на оставащо време…",
|
||||
"checking_files": "Проверка на файловете за {{title}}… ({{percentage}} завършено)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Инсталацията завършена",
|
||||
"installation_complete_message": "Общите компоненти са инсталирани успешно"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Филтър…",
|
||||
"search": "Филтрирай…",
|
||||
"developers": "Разработчици",
|
||||
"genres": "Жанрове",
|
||||
"tags": "Тагове",
|
||||
"publishers": "Издатели",
|
||||
"download_sources": "Източници за изтегляне",
|
||||
"result_count": "{{resultCount}} резултати",
|
||||
"result_count": "{{resultCount}} резултата",
|
||||
"filter_count": "{{filterCount}} налични",
|
||||
"clear_filters": "Изчисти {{filterCount}} избрани"
|
||||
},
|
||||
"game_details": {
|
||||
"launch_options": "Опции за стартиране",
|
||||
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране (экспериментальный)",
|
||||
"launch_options_placeholder": "Няма зададен параметър",
|
||||
"open_download_options": "Варианти за изтегляне",
|
||||
"download_options_zero": "Няма варианти за изтегляне",
|
||||
"download_options_one": "{{count}} варианти за изтегляне",
|
||||
"download_options_other": "{{count}} варианти за изтегляне",
|
||||
"open_download_options": "Отвори опциите за изтегляне",
|
||||
"download_options_zero": "Няма опции за изтегляне",
|
||||
"download_options_one": "{{count}} опция за изтегляне",
|
||||
"download_options_other": "{{count}} опции за изтегляне",
|
||||
"updated_at": "Обновено на {{updated_at}}",
|
||||
"install": "Инсталирай",
|
||||
"resume": "Продължи",
|
||||
"pause": "Пауза",
|
||||
"cancel": "Отказ",
|
||||
"remove": "Премахни",
|
||||
"space_left_on_disk": "{{space}} място на диска",
|
||||
"eta": "Заклчение {{eta}}",
|
||||
"calculating_eta": "Калкулиране на оставащо време…",
|
||||
"downloading_metadata": "Изтегляне на метадата…",
|
||||
"filter": "Филтрирай repacks",
|
||||
"space_left_on_disk": "{{space}} свободно на диска",
|
||||
"eta": "Завършване {{eta}}",
|
||||
"calculating_eta": "Изчисляване на оставащо време…",
|
||||
"downloading_metadata": "Изтегляне на метаданни…",
|
||||
"filter": "Филтрирай репаковки",
|
||||
"requirements": "Системни изисквания",
|
||||
"minimum": "Минимални",
|
||||
"recommended": "Препоръчителни",
|
||||
"paused": "Паузирано",
|
||||
"release_date": "Издадено на {{date}}",
|
||||
"publisher": "Публикувано от {{publisher}}",
|
||||
"hours": "часове",
|
||||
"paused": "На пауза",
|
||||
"release_date": "Издадена на {{date}}",
|
||||
"publisher": "Издател: {{publisher}}",
|
||||
"hours": "часа",
|
||||
"minutes": "минути",
|
||||
"amount_hours": "{{amount}} часа",
|
||||
"amount_minutes": "{{amount}} минути",
|
||||
@@ -90,333 +90,425 @@
|
||||
"add_to_library": "Добави в библиотеката",
|
||||
"remove_from_library": "Премахни от библиотеката",
|
||||
"no_downloads": "Няма налични изтегляния",
|
||||
"play_time": "Игрално време {{amount}}",
|
||||
"last_time_played": "Последно пускане {{period}}",
|
||||
"not_played_yet": "Не сте играли {{title}} все още",
|
||||
"play_time": "Играно: {{amount}}",
|
||||
"last_time_played": "Последно играно: {{period}}",
|
||||
"not_played_yet": "Все още не сте играли {{title}}",
|
||||
"next_suggestion": "Следващо предложение",
|
||||
"play": "Пускане",
|
||||
"deleting": "Изтриване на инсталация…",
|
||||
"play": "Играй",
|
||||
"deleting": "Изтриване на инсталатора…",
|
||||
"close": "Затвори",
|
||||
"playing_now": "Играй сега",
|
||||
"change": "Промяна",
|
||||
"repacks_modal_description": "Избери repack който искаш да изтеглиш",
|
||||
"select_folder_hint": "За да промените стандартната папка отидете в <0>Настройки</0>",
|
||||
"playing_now": "Играе се сега",
|
||||
"change": "Промени",
|
||||
"repacks_modal_description": "Изберете репак за изтегляне",
|
||||
"select_folder_hint": "За да промените папката по подразбиране, отидете в <0>Настройки</0>",
|
||||
"download_now": "Изтегли сега",
|
||||
"no_shop_details": "Не може да се извлекат данни за магазина.",
|
||||
"download_options": "Опции за сваляне",
|
||||
"download_path": "Път за сваляне",
|
||||
"previous_screenshot": "Предишна снимка",
|
||||
"next_screenshot": "Следваща снимка",
|
||||
"screenshot": "Снимка {{number}}",
|
||||
"open_screenshot": "Отвори снимки {{number}}",
|
||||
"download_settings": "Настройки за сваляне",
|
||||
"downloader": "Downloader",
|
||||
"no_shop_details": "Неуспешно извличане на детайли от магазина.",
|
||||
"download_options": "Опции за изтегляне",
|
||||
"download_path": "Път за изтегляне",
|
||||
"previous_screenshot": "Предишен скрийншот",
|
||||
"next_screenshot": "Следващ скрийншот",
|
||||
"screenshot": "Скрийншот {{number}}",
|
||||
"open_screenshot": "Отвори скрийншот {{number}}",
|
||||
"download_settings": "Настройки за изтегляне",
|
||||
"downloader": "Изтегляч",
|
||||
"select_executable": "Избери",
|
||||
"no_executable_selected": "Няма избран стартиращ файл",
|
||||
"no_executable_selected": "Няма избран изпълним файл",
|
||||
"open_folder": "Отвори папка",
|
||||
"open_download_location": "Виж свалените файлове",
|
||||
"create_shortcut": "Пряк път на Десктопа",
|
||||
"open_download_location": "Виж изтеглените файлове",
|
||||
"create_shortcut": "Създай пряк път на работния плот",
|
||||
"clear": "Изчисти",
|
||||
"remove_files": "Премахни файловете",
|
||||
"remove_from_library_title": "Сигурен ли си?",
|
||||
"remove_from_library_description": "Това ще премахне {{game}} от Библиотеката",
|
||||
"remove_from_library_title": "Сигурни ли сте?",
|
||||
"remove_from_library_description": "Това ще премахне {{game}} от вашата библиотека",
|
||||
"options": "Опции",
|
||||
"executable_section_title": "Стартиращ файл",
|
||||
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Пускане\"",
|
||||
"downloads_section_title": "Свалени",
|
||||
"downloads_section_description": "Вижте актуализации или други версии на тази игра",
|
||||
"executable_section_title": "Изпълним файл",
|
||||
"executable_section_description": "Пътят на файла, който ще се изпълни при \"Играй\"",
|
||||
"downloads_section_title": "Изтегляния",
|
||||
"downloads_section_description": "Вижте обновления или други версии на тази игра",
|
||||
"danger_zone_section_title": "Опасна зона",
|
||||
"danger_zone_section_description": "Премахнете тази игра от библиотеката си или от файловете, изтеглени от Hydra",
|
||||
"download_in_progress": "Изтегляне в ход",
|
||||
"download_paused": "Изтеглянето е паузирано",
|
||||
"last_downloaded_option": "Опция от последно изтегляне",
|
||||
"danger_zone_section_description": "Премахнете тази игра от библиотеката или файловете, изтеглени от Hydra",
|
||||
"download_in_progress": "Изтеглянето е в ход",
|
||||
"download_paused": "Изтеглянето е на пауза",
|
||||
"last_downloaded_option": "Последно изтеглена опция",
|
||||
"create_steam_shortcut": "Създай пряк път за Steam",
|
||||
"create_shortcut_success": "Прекият път е създаден успешно",
|
||||
"create_shortcut_error": "Грешка при създаването на пряк път",
|
||||
"you_might_need_to_restart_steam": "Може да е необходимо да рестартирате Steam, за да видите промените",
|
||||
"create_shortcut_error": "Грешка при създаване на пряк път",
|
||||
"nsfw_content_title": "Тази игра съдържа неподходящо съдържание",
|
||||
"nsfw_content_description": "{{title}} съдържа съдържание, което може да не е подходящо за всички възрасти. Сигурни ли сте, че искате да продължите?",
|
||||
"allow_nsfw_content": "Продължи",
|
||||
"refuse_nsfw_content": "Назад",
|
||||
"refuse_nsfw_content": "Върни се",
|
||||
"stats": "Статистики",
|
||||
"download_count": "Сваляния",
|
||||
"download_count": "Изтегляния",
|
||||
"player_count": "Активни играчи",
|
||||
"download_error": "Тази опция за изтегляне не е налична",
|
||||
"download": "Свали",
|
||||
"download": "Изтегли",
|
||||
"executable_path_in_use": "Изпълнимият файл вече се използва от \"{{game}}\"",
|
||||
"warning": "Внимание:",
|
||||
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена, когато е завършено. Ако Hydra се затвори преди завършването, ще загубите напредъка си..",
|
||||
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена до завършване. Ако затворите преди завършване, ще загубите прогреса.",
|
||||
"achievements": "Постижения",
|
||||
"achievements_count": "Постижения {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Запазване в облака",
|
||||
"cloud_save_description": "Запазете напредъка си в облака и продължете да играете на всяко устройство",
|
||||
"backups": "Резервни копия",
|
||||
"cloud_save": "Облачно запазване",
|
||||
"cloud_save_description": "Запазете прогреса си в облака и продължете да играете на всяко устройство",
|
||||
"backups": "Архиви",
|
||||
"install_backup": "Инсталирай",
|
||||
"delete_backup": "Изтрий",
|
||||
"create_backup": "Ново копие",
|
||||
"last_backup_date": "Последно копие от {{date}}",
|
||||
"no_backup_preview": "Не бяха намерени запазени игри за това заглавие",
|
||||
"restoring_backup": "Възстановяване на резервно копие ({{progress}} готово)…",
|
||||
"uploading_backup": "Качване на резервно копие…",
|
||||
"no_backups": "Все още не сте създали резервни копия за тази игра",
|
||||
"backup_uploaded": "Качено резервно копие",
|
||||
"backup_deleted": "Изтрито резервно копие",
|
||||
"backup_restored": "Възстановен бекъп",
|
||||
"see_all_achievements": "Вижте всички постижения",
|
||||
"create_backup": "Нов архив",
|
||||
"last_backup_date": "Последен архив на {{date}}",
|
||||
"no_backup_preview": "Не са намерени запазени игри за това заглавие",
|
||||
"restoring_backup": "Възстановяване на архив ({{progress}} завършено)…",
|
||||
"uploading_backup": "Качване на архив…",
|
||||
"no_backups": "Не сте създали архиви за тази игра",
|
||||
"backup_uploaded": "Архивът е качен",
|
||||
"backup_deleted": "Архивът е изтрит",
|
||||
"backup_restored": "Архивът е възстановен",
|
||||
"see_all_achievements": "Виж всички постижения",
|
||||
"sign_in_to_see_achievements": "Влезте, за да видите постиженията",
|
||||
"mapping_method_automatic": "Автоматично",
|
||||
"mapping_method_manual": "Ръчно",
|
||||
"mapping_method_label": "Метод на картографиране",
|
||||
"files_automatically_mapped": "Автоматично картографиране на файлове",
|
||||
"no_backups_created": "Не са създадени резервни копия за тази игра",
|
||||
"manage_files": "Управление на файлове",
|
||||
"mapping_method_label": "Метод на съпоставяне",
|
||||
"files_automatically_mapped": "Файловете са съпоставени автоматично",
|
||||
"no_backups_created": "Няма създадени архиви за тази игра",
|
||||
"manage_files": "Управлявай файлове",
|
||||
"loading_save_preview": "Търсене на запазени игри…",
|
||||
"wine_prefix": "Wine Префикс",
|
||||
"wine_prefix_description": "Wine prefix използван за тази игра",
|
||||
"no_download_option_info": "Няма налични данни",
|
||||
"backup_deletion_failed": "Неуспешно изтриване на резервно копие",
|
||||
"max_number_of_artifacts_reached": "Достигнат максимален брой резервни копия за тази игра",
|
||||
"achievements_not_sync": "Постиженията не са синхронизирани",
|
||||
"manage_files_description": "Управлявайте кои файлове ще бъдат архивирани и възстановени",
|
||||
"wine_prefix": "Wine префикс",
|
||||
"wine_prefix_description": "Wine префикс, използван за стартиране на тази игра",
|
||||
"launch_options": "Опции за стартиране",
|
||||
"launch_options_description": "Напреднали потребители могат да въведат модификации (експериментална функция)",
|
||||
"launch_options_placeholder": "Няма зададен параметър",
|
||||
"no_download_option_info": "Няма налична информация",
|
||||
"backup_deletion_failed": "Неуспешно изтриване на архив",
|
||||
"max_number_of_artifacts_reached": "Достигнат е максималният брой архиви за тази игра",
|
||||
"achievements_not_sync": "Вижте как да синхронизирате постиженията си",
|
||||
"manage_files_description": "Управлявайте кои файлове ще се архивират и възстановяват",
|
||||
"select_folder": "Избери папка",
|
||||
"backup_from": "Резервно копие от {{date}}",
|
||||
"custom_backup_location_set": "Задаване на персонализирано местоположение за архивиране"
|
||||
"backup_from": "Архив от {{date}}",
|
||||
"automatic_backup_from": "Автоматичен архив от {{date}}",
|
||||
"enable_automatic_cloud_sync": "Включи автоматична синхронизация с облака",
|
||||
"custom_backup_location_set": "Зададено е персонализирано място за архив",
|
||||
"no_directory_selected": "Няма избрана директория",
|
||||
"no_write_permission": "Не може да се изтегли в тази директория. Кликнете тук за повече информация.",
|
||||
"reset_achievements": "Нулирай постиженията",
|
||||
"reset_achievements_description": "Това ще нулира всички постижения за {{game}}",
|
||||
"reset_achievements_title": "Сигурни ли сте?",
|
||||
"reset_achievements_success": "Постиженията са нулирани успешно",
|
||||
"reset_achievements_error": "Неуспешно нулиране на постиженията",
|
||||
"download_error_gofile_quota_exceeded": "Превишихте месечната си квота в Gofile. Моля, изчакайте тя да се възстанови.",
|
||||
"download_error_real_debrid_account_not_authorized": "Вашият Real-Debrid акаунт не е упълномощен за нови изтегляния. Моля, проверете настройките на акаунта и опитайте отново.",
|
||||
"download_error_not_cached_on_real_debrid": "Това изтегляне не е налично в Real-Debrid и не може да се следи статуса.",
|
||||
"download_error_not_cached_on_torbox": "Това изтегляне не е налично в TorBox и не може да се следи статуса.",
|
||||
"download_error_not_cached_on_hydra": "Това изтегляне не е налично в Nimbus.",
|
||||
"game_removed_from_favorites": "Играта е премахната от любими",
|
||||
"game_added_to_favorites": "Играта е добавена в любими",
|
||||
"automatically_extract_downloaded_files": "Автоматично извличане на изтеглени файлове",
|
||||
"create_start_menu_shortcut": "Създай пряк път в старт менюто",
|
||||
"invalid_wine_prefix_path": "Невалиден път до Wine префикса",
|
||||
"invalid_wine_prefix_path_description": "Пътят до Wine префикса е невалиден. Моля, проверете го и опитайте отново.",
|
||||
"missing_wine_prefix": "Wine префикс е необходим за създаване на архив в Linux"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активирай Hydra",
|
||||
"installation_id": "Идентификатор на инсталацията:",
|
||||
"enter_activation_code": "Въведете кода за активиране",
|
||||
"message": "Ако не знаете къде да попитате за това, значи не трябва да го имате..",
|
||||
"installation_id": "Инсталационен ID:",
|
||||
"enter_activation_code": "Въведете активационен код",
|
||||
"message": "Ако не знаете къде да попитате за това, не бива да го имате.",
|
||||
"activate": "Активирай",
|
||||
"loading": "Зареждане…"
|
||||
},
|
||||
"downloads": {
|
||||
"seeding": "Сийдване",
|
||||
"stop_seeding": "Спри сийдването",
|
||||
"resume_seeding": "Продължи сийдването",
|
||||
"options": "Управление",
|
||||
"resume": "Продължи",
|
||||
"pause": "Пауза",
|
||||
"eta": "Conclusion {{eta}}",
|
||||
"paused": "Паузирано",
|
||||
"eta": "Завършване {{eta}}",
|
||||
"paused": "На пауза",
|
||||
"verifying": "Проверка…",
|
||||
"completed": "Готово",
|
||||
"removed": "Не е изтеглен",
|
||||
"completed": "Завършено",
|
||||
"removed": "Не е изтеглено",
|
||||
"cancel": "Отказ",
|
||||
"filter": "Филтриране на изтеглени игри",
|
||||
"filter": "Филтрирай изтеглените игри",
|
||||
"remove": "Премахни",
|
||||
"downloading_metadata": "Изтегляне на метаданни…",
|
||||
"deleting": "Изтриване на инсталатора…",
|
||||
"delete": "Премахване на инсталатора",
|
||||
"delete_modal_title": "Сигурени ли сте?",
|
||||
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви.",
|
||||
"delete": "Премахни инсталатора",
|
||||
"delete_modal_title": "Сигурни ли сте?",
|
||||
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви",
|
||||
"install": "Инсталирай",
|
||||
"download_in_progress": "В процес на изпълнение",
|
||||
"queued_downloads": "Изтеглени файлове в опашката",
|
||||
"downloads_completed": "Приключени",
|
||||
"queued": "В опашка",
|
||||
"download_in_progress": "В процес",
|
||||
"queued_downloads": "Изтегляния на опашка",
|
||||
"downloads_completed": "Завършени",
|
||||
"queued": "В опашката",
|
||||
"no_downloads_title": "Толкова е празно",
|
||||
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете...",
|
||||
"checking_files": "Проверка на файлове…"
|
||||
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете.",
|
||||
"checking_files": "Проверка на файлове…",
|
||||
"seeding": "Сийдване",
|
||||
"stop_seeding": "Спри сийдването",
|
||||
"resume_seeding": "Продължи сийдването",
|
||||
"options": "Управлявай",
|
||||
"extract": "Извлечи файловете",
|
||||
"extracting": "Извличане на файловете…"
|
||||
},
|
||||
"settings": {
|
||||
"seed_after_download_complete": "Сийд след завършване на изтеглянето",
|
||||
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключването им",
|
||||
"downloads_path": "Инсталационен път",
|
||||
"change": "Актуализиране",
|
||||
"downloads_path": "Път за изтегляния",
|
||||
"change": "Обнови",
|
||||
"notifications": "Известия",
|
||||
"enable_download_notifications": "Когато изтеглянето е завършено",
|
||||
"enable_repack_list_notifications": "Когато се добави нов repack",
|
||||
"enable_download_notifications": "Когато изтеглянето приключи",
|
||||
"enable_repack_list_notifications": "Когато бъде добавен нов репак",
|
||||
"real_debrid_api_token_label": "Real-Debrid API токен",
|
||||
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
|
||||
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
|
||||
"quit_app_instead_hiding": "Не скривай Hydra при затваряне",
|
||||
"launch_with_system": "Стартирай Hydra при стартиране на системата",
|
||||
"general": "Общи",
|
||||
"behavior": "Поведение",
|
||||
"download_sources": "Източници за изтегляне",
|
||||
"language": "Език",
|
||||
"api_token": "API Токен",
|
||||
"api_token": "API токен",
|
||||
"enable_real_debrid": "Включи Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
||||
"real_debrid_description": "Real-Debrid е неограничен изтегляч, който ви позволява да теглите бързо, ограничено само от интернет връзката ви.",
|
||||
"debrid_invalid_token": "Невалиден API токен",
|
||||
"debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
||||
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
||||
"debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
||||
"debrid_api_token_hint": "Може да получите вашия API токен <0>тук</0>",
|
||||
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен. Моля, абонирайте се за Real-Debrid",
|
||||
"debrid_linked_message": "Акаунт \"{{username}}\" е свързан",
|
||||
"save_changes": "Запази промените",
|
||||
"changes_saved": "Промените са успешно запазни",
|
||||
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
||||
"validate_download_source": "Валидиране",
|
||||
"changes_saved": "Промените са запазени успешно",
|
||||
"download_sources_description": "Hydra ще взема линкове за изтегляне от тези източници. URL адресът трябва да сочи към .json файл с линкове.",
|
||||
"validate_download_source": "Валидирай",
|
||||
"remove_download_source": "Премахни",
|
||||
"add_download_source": "Добави източник",
|
||||
"download_count_zero": "Няма опции за сваляне",
|
||||
"download_count_one": "{{countFormatted}} опции за сваляне",
|
||||
"download_count_other": "{{countFormatted}} опции за сваляне",
|
||||
"download_source_url": "URL адрес на източника за изтегляне",
|
||||
"add_download_source_description": "Вмъкнете URL адреса на файла .json",
|
||||
"download_source_up_to_date": "Актуален",
|
||||
"download_source_errored": "Сгрешен",
|
||||
"sync_download_sources": "Синхронизирай източниците",
|
||||
"removed_download_source": "Източника за сваляне е премахнат",
|
||||
"cancel_button_confirmation_delete_all_sources": "не",
|
||||
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
||||
"description_confirmation_delete_all_sources": "Вы удалите все источники загрузки",
|
||||
"title_confirmation_delete_all_sources": "Удалить все источники загрузки",
|
||||
"removed_download_sources": "Шрифты удалены",
|
||||
"button_delete_all_sources": "Удалить все источники загрузки",
|
||||
"added_download_source": "Добавен източник за сваляне",
|
||||
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
|
||||
"insert_valid_json_url": "Добавете ваиден JSON линк",
|
||||
"found_download_option_zero": "Няма намерени опции за сваляне",
|
||||
"found_download_option_one": "Намерени {{countFormatted}} опции за сваляне",
|
||||
"found_download_option_other": "Намерени {{countFormatted}} опции за сваляне",
|
||||
"import": "Внеси",
|
||||
"public": "Публичен",
|
||||
"private": "Личен",
|
||||
"download_count_zero": "Няма опции за изтегляне",
|
||||
"download_count_one": "{{countFormatted}} опция за изтегляне",
|
||||
"download_count_other": "{{countFormatted}} опции за изтегляне",
|
||||
"download_source_url": "URL на източника",
|
||||
"add_download_source_description": "Въведете URL на .json файла",
|
||||
"download_source_up_to_date": "Актуализиран",
|
||||
"download_source_errored": "Грешка",
|
||||
"sync_download_sources": "Синхронизирай източници",
|
||||
"removed_download_source": "Източникът е премахнат",
|
||||
"removed_download_sources": "Източниците са премахнати",
|
||||
"cancel_button_confirmation_delete_all_sources": "Не",
|
||||
"confirm_button_confirmation_delete_all_sources": "Да, изтрий всичко",
|
||||
"title_confirmation_delete_all_sources": "Изтрий всички източници",
|
||||
"description_confirmation_delete_all_sources": "Ще изтриете всички източници",
|
||||
"button_delete_all_sources": "Премахни всички",
|
||||
"added_download_source": "Източникът е добавен",
|
||||
"download_sources_synced": "Всички източници са синхронизирани",
|
||||
"insert_valid_json_url": "Въведете валиден JSON url",
|
||||
"found_download_option_zero": "Не е намерена опция за изтегляне",
|
||||
"found_download_option_one": "Намерена е {{countFormatted}} опция за изтегляне",
|
||||
"found_download_option_other": "Намерени са {{countFormatted}} опции за изтегляне",
|
||||
"import": "Импортирай",
|
||||
"public": "Публично",
|
||||
"private": "Частно",
|
||||
"friends_only": "Само за приятели",
|
||||
"privacy": "Поверителност",
|
||||
"profile_visibility": "Видимост на профила",
|
||||
"profile_visibility_description": "Изберете кой може да вижда вашия профил и библиотека",
|
||||
"required_field": "Това поле е задължително",
|
||||
"source_already_exists": "Този източник вече е добавен",
|
||||
"must_be_valid_url": "Източникът трябва да е валиден URL адрес.",
|
||||
"must_be_valid_url": "Източникът трябва да е валиден URL",
|
||||
"blocked_users": "Блокирани потребители",
|
||||
"user_unblocked": "Потребителят е бил деблокиран",
|
||||
"enable_achievement_notifications": "Когато е отключено постижение",
|
||||
"launch_minimized": "Стартиране на Hydra минимизирано",
|
||||
"disable_nsfw_alert": "Деактивиране на предупреждението NSFW"
|
||||
"user_unblocked": "Потребителят е деблокиран",
|
||||
"enable_achievement_notifications": "Когато бъде отключено постижение",
|
||||
"launch_minimized": "Стартирай Hydra минимизирано",
|
||||
"disable_nsfw_alert": "Изключи NSFW предупреждението",
|
||||
"seed_after_download_complete": "Сийдвай след завършване на изтеглянето",
|
||||
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключване",
|
||||
"account": "Акаунт",
|
||||
"no_users_blocked": "Нямате блокирани потребители",
|
||||
"subscription_active_until": "Hydra Cloud е активен до {{date}}",
|
||||
"manage_subscription": "Управлявай абонамента",
|
||||
"update_email": "Обнови имейл",
|
||||
"update_password": "Обнови парола",
|
||||
"current_email": "Текущ имейл:",
|
||||
"no_email_account": "Все още не сте задали имейл",
|
||||
"account_data_updated_successfully": "Данните на акаунта са обновени успешно",
|
||||
"renew_subscription": "Поднови Hydra Cloud",
|
||||
"subscription_expired_at": "Абонаментът изтече на {{date}}",
|
||||
"no_subscription": "Наслаждавайте се на Hydra по най-добрия начин",
|
||||
"become_subscriber": "Станете абонат на Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Автоматичното подновяване е изключено",
|
||||
"subscription_renews_on": "Абонаментът се подновява на {{date}}",
|
||||
"bill_sent_until": "Следващата фактура ще бъде изпратена до този ден",
|
||||
"no_themes": "Изглежда, че все още нямате теми. Кликнете тук, за да създадете първата си.",
|
||||
"editor_tab_code": "Код",
|
||||
"editor_tab_info": "Информация",
|
||||
"editor_tab_save": "Запази",
|
||||
"web_store": "Уеб магазин",
|
||||
"clear_themes": "Изчисти",
|
||||
"create_theme": "Създай",
|
||||
"create_theme_modal_title": "Създай персонализирана тема",
|
||||
"create_theme_modal_description": "Създайте нова тема за персонализиране на външния вид на Hydra",
|
||||
"theme_name": "Име",
|
||||
"insert_theme_name": "Въведете име на тема",
|
||||
"set_theme": "Задай тема",
|
||||
"unset_theme": "Премахни тема",
|
||||
"delete_theme": "Изтрий тема",
|
||||
"edit_theme": "Редактирай тема",
|
||||
"delete_all_themes": "Изтрий всички теми",
|
||||
"delete_all_themes_description": "Това ще изтрие всички ваши персонализирани теми",
|
||||
"delete_theme_description": "Това ще изтрие темата {{theme}}",
|
||||
"cancel": "Отказ",
|
||||
"appearance": "Външен вид",
|
||||
"enable_torbox": "Включи TorBox",
|
||||
"torbox_description": "TorBox е вашият премиум seedbox, съперничещ на най-добрите сървъри на пазара.",
|
||||
"torbox_account_linked": "TorBox акаунтът е свързан",
|
||||
"create_real_debrid_account": "Кликнете тук, ако все още нямате Real-Debrid акаунт",
|
||||
"create_torbox_account": "Кликнете тук, ако все още нямате TorBox акаунт",
|
||||
"real_debrid_account_linked": "Real-Debrid акаунтът е свързан",
|
||||
"name_min_length": "Името на темата трябва да е поне 3 символа",
|
||||
"import_theme": "Импортирай тема",
|
||||
"import_theme_description": "Ще импортирате {{theme}} от магазина с теми",
|
||||
"error_importing_theme": "Грешка при импортиране на тема",
|
||||
"theme_imported": "Темата е импортирана успешно",
|
||||
"enable_friend_request_notifications": "Когато получите заявка за приятелство",
|
||||
"enable_auto_install": "Автоматично изтегляй обновления",
|
||||
"common_redist": "Общи компоненти",
|
||||
"common_redist_description": "Общите компоненти са нужни за някои игри. Препоръчва се инсталация.",
|
||||
"install_common_redist": "Инсталирай",
|
||||
"installing_common_redist": "Инсталиране…",
|
||||
"show_download_speed_in_megabytes": "Показвай скоростта на изтегляне в MB/s",
|
||||
"extract_files_by_default": "Извличай файловете по подразбиране след изтегляне"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Изтеглянето е завършено",
|
||||
"game_ready_to_install": "{{title}} е готово за инсталиране",
|
||||
"repack_list_updated": "Repack лист е обновен",
|
||||
"repack_count_one": "{{count}} repack е добавен",
|
||||
"repack_count_other": "{{count}} repacks добавени",
|
||||
"new_update_available": "Версия {{version}} е налична",
|
||||
"restart_to_install_update": "Рестартирайте Hydra, за да инсталирате актуализацията",
|
||||
"download_complete": "Изтеглянето завърши",
|
||||
"game_ready_to_install": "{{title}} е готова за инсталация",
|
||||
"repack_list_updated": "Списъкът с репаци е обновен",
|
||||
"repack_count_one": "Добавен е {{count}} репак",
|
||||
"repack_count_other": "Добавени са {{count}} репака",
|
||||
"new_update_available": "Налична е версия {{version}}",
|
||||
"restart_to_install_update": "Рестартирайте Hydra за инсталиране на обновлението",
|
||||
"notification_achievement_unlocked_title": "Отключено постижение за {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} и други {{count}} са отклщчени"
|
||||
"notification_achievement_unlocked_body": "{{achievement}} и още {{count}} бяха отключени",
|
||||
"new_friend_request_description": "{{displayName}} ви изпрати заявка за приятелство",
|
||||
"new_friend_request_title": "Нова заявка за приятелство",
|
||||
"extraction_complete": "Извличането завърши",
|
||||
"game_extracted": "{{title}} е извлечена успешно",
|
||||
"friend_started_playing_game": "{{displayName}} започна да играе игра"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Отвори Hydra",
|
||||
"quit": "Изход"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Налично",
|
||||
"available_other": "Налично",
|
||||
"no_downloads": "Няма налични изтегляния"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Не инсталирани програми",
|
||||
"description": "Wine или Lutris изпълними файлове не бяха открити на вашата система",
|
||||
"instructions": "Проверете правилния начин за инсталиране на някоя от тях на вашата дистрибуция на Linux, за да може играта да работи нормално"
|
||||
"title": "Програмите не са инсталирани",
|
||||
"description": "Wine или Lutris не са открити на вашата система",
|
||||
"instructions": "Проверете как да инсталирате някоя от тях за вашата Linux дистрибуция, за да може играта да работи."
|
||||
},
|
||||
"modal": {
|
||||
"close": "Бутон за затваряне"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Превключване на видимостта на паролата"
|
||||
"toggle_password_visibility": "Показване/скриване на паролата"
|
||||
},
|
||||
"user_profile": {
|
||||
"stats": "Статистики",
|
||||
"achievements": "Постижения",
|
||||
"games": "Игри",
|
||||
"top_percentile": "Топ {{percentile}}%",
|
||||
"ranking_updated_weekly": "Класацията се актуализира седмично",
|
||||
"playing": "Играе {{game}}",
|
||||
"achievements_unlocked": "Отключени постижения",
|
||||
"earned_points": "Спечелени точки",
|
||||
"show_achievements_on_profile": "Показвай своите постижения в профила",
|
||||
"show_points_on_profile": "Показвай спечелените точки в профила",
|
||||
"amount_hours": "{{amount}} часове",
|
||||
"amount_hours": "{{amount}} часа",
|
||||
"amount_minutes": "{{amount}} минути",
|
||||
"last_time_played": "Последно играно {{period}}",
|
||||
"activity": "Скорошна активност",
|
||||
"last_time_played": "Последно играно: {{period}}",
|
||||
"activity": "Последна активност",
|
||||
"library": "Библиотека",
|
||||
"total_play_time": "Общо време за игра",
|
||||
"no_recent_activity_title": "Хмм… няма нищо тук",
|
||||
"no_recent_activity_description": "Не сте играли игри напоследък. Време е да промените това.!",
|
||||
"display_name": "Показване на името",
|
||||
"no_recent_activity_title": "Хммм… няма нищо тук",
|
||||
"no_recent_activity_description": "Не сте играли игри наскоро. Време е да го промените!",
|
||||
"display_name": "Показвано име",
|
||||
"saving": "Запазване",
|
||||
"save": "Запис",
|
||||
"edit_profile": "Редактиране на профила",
|
||||
"saved_successfully": "Запазено успешно",
|
||||
"try_again": "Моля, опитайте пак",
|
||||
"save": "Запази",
|
||||
"edit_profile": "Редактирай профил",
|
||||
"saved_successfully": "Успешно запазено",
|
||||
"try_again": "Моля, опитайте отново",
|
||||
"sign_out_modal_title": "Сигурни ли сте?",
|
||||
"cancel": "Отказ",
|
||||
"successfully_signed_out": "Успешно се отписахте",
|
||||
"sign_out": "Отписване",
|
||||
"playing_for": "В игра от {{amount}}",
|
||||
"sign_out_modal_text": "Вашата библиотека е свързана с текущата ви сметка. Когато се отпишете, библиотеката ви вече няма да е видима и напредъкът няма да бъде запазен. Продължете с отписването?",
|
||||
"successfully_signed_out": "Успешно излязохте",
|
||||
"sign_out": "Изход",
|
||||
"playing_for": "Играе се от {{amount}}",
|
||||
"sign_out_modal_text": "Библиотеката ви е свързана с този акаунт. При изход, тя няма да е видима, а прогресът няма да се запази. Продължавате ли?",
|
||||
"add_friends": "Добави приятели",
|
||||
"add": "Добави",
|
||||
"friend_code": "Приятелски код",
|
||||
"friend_code": "Код за приятелство",
|
||||
"see_profile": "Виж профила",
|
||||
"sending": "Изпращане",
|
||||
"friend_request_sent": "Изпратена покана за приятелство",
|
||||
"friend_request_sent": "Заявката е изпратена",
|
||||
"friends": "Приятели",
|
||||
"friends_list": "Списък с приятели",
|
||||
"user_not_found": "Не е намерен потребител",
|
||||
"user_not_found": "Потребителят не е намерен",
|
||||
"block_user": "Блокирай потребител",
|
||||
"add_friend": "Добави приятел",
|
||||
"request_sent": "Изпратена покана",
|
||||
"request_received": "Получена покана",
|
||||
"accept_request": "Приеми поканата",
|
||||
"ignore_request": "Игнирирай поканата",
|
||||
"cancel_request": "Откажи поканата",
|
||||
"undo_friendship": "Отмяна на приятелството",
|
||||
"request_accepted": "Поканата е приета",
|
||||
"request_sent": "Заявката е изпратена",
|
||||
"request_received": "Получена заявка",
|
||||
"accept_request": "Приеми заявката",
|
||||
"ignore_request": "Игнорирай заявката",
|
||||
"cancel_request": "Отмени заявката",
|
||||
"undo_friendship": "Премахни приятелството",
|
||||
"request_accepted": "Заявката е приета",
|
||||
"user_blocked_successfully": "Потребителят е блокиран успешно",
|
||||
"user_block_modal_text": "Това ще блокира {{displayName}}",
|
||||
"blocked_users": "Блокирани потребители",
|
||||
"unblock": "Отблокирай",
|
||||
"no_friends_added": "Не сте добавили приятели",
|
||||
"unblock": "Деблокирай",
|
||||
"no_friends_added": "Нямате добавени приятели",
|
||||
"pending": "Чакащи",
|
||||
"no_pending_invites": "Нямате чакащи покани",
|
||||
"no_blocked_users": "Нямате блокирани потребители",
|
||||
"friend_code_copied": "Приятелския код е копиран",
|
||||
"undo_friendship_modal_text": "Това ще отмени приятелството ви с {{displayName}}",
|
||||
"privacy_hint": "За да настроите кой може да вижда това, отидете в <0>Настройки</0>",
|
||||
"locked_profile": "Този профил е личен",
|
||||
"image_process_failure": "Грешка при обработката на изображението",
|
||||
"friend_code_copied": "Кодът за приятелство е копиран",
|
||||
"undo_friendship_modal_text": "Това ще премахне приятелството ви с {{displayName}}",
|
||||
"privacy_hint": "За да промените кой вижда това, отидете в <0>Настройки</0>",
|
||||
"locked_profile": "Този профил е частен",
|
||||
"image_process_failure": "Грешка при обработка на изображението",
|
||||
"required_field": "Това поле е задължително",
|
||||
"displayname_min_length": "Името трябва да е дълго поне 3 символа",
|
||||
"displayname_max_length": "Името трябва да е с дължина не повече от 50 символа.",
|
||||
"displayname_min_length": "Показваното име трябва да съдържа поне 3 символа",
|
||||
"displayname_max_length": "Показваното име трябва да съдържа най-много 50 символа",
|
||||
"report_profile": "Докладвай този профил",
|
||||
"report_reason": "Защо докладвате този профил?",
|
||||
"report_description": "Допълнителна информация",
|
||||
"report_description_placeholder": "Допълнителна информация",
|
||||
"report": "Докладвай",
|
||||
"report_reason_hate": "Омразна реч",
|
||||
"report_reason_hate": "Реч на омразата",
|
||||
"report_reason_sexual_content": "Сексуално съдържание",
|
||||
"report_reason_violence": "Насилия",
|
||||
"report_reason_violence": "Насилие",
|
||||
"report_reason_spam": "Спам",
|
||||
"report_reason_other": "Друго",
|
||||
"profile_reported": "Профилът е докладван",
|
||||
"your_friend_code": "Вашия приятелски код:",
|
||||
"your_friend_code": "Вашият код за приятелство:",
|
||||
"upload_banner": "Качи банер",
|
||||
"uploading_banner": "Качване на банер…",
|
||||
"background_image_updated": "Обновено фоново изображение"
|
||||
"uploading_banner": "Качване на банера…",
|
||||
"background_image_updated": "Фоновото изображение е обновено",
|
||||
"stats": "Статистики",
|
||||
"achievements": "постижения",
|
||||
"games": "Игри",
|
||||
"top_percentile": "Топ {{percentile}}%",
|
||||
"ranking_updated_weekly": "Класацията се обновява седмично",
|
||||
"playing": "Играе {{game}}",
|
||||
"achievements_unlocked": "Отключени постижения",
|
||||
"earned_points": "Спечелени точки",
|
||||
"show_achievements_on_profile": "Показвай постиженията в профила",
|
||||
"show_points_on_profile": "Показвай спечелените точки в профила"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Отключено постижение",
|
||||
"user_achievements": "Постижения на {{displayName}}",
|
||||
"your_achievements": "Вашите постижения",
|
||||
"unlocked_at": "Отключено на: {{date}}",
|
||||
"subscription_needed": "Изисква се абонамент за Hydra Cloud за този съдържание",
|
||||
"new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игри",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения",
|
||||
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Това е скрито постижение",
|
||||
"achievement_earn_points": "Спечели {{points}} точки с това постижение",
|
||||
"achievement_earn_points": "Спечелете {{points}} точки с това постижение",
|
||||
"earned_points": "Спечелени точки:",
|
||||
"available_points": "Налични точки:",
|
||||
"how_to_earn_achievements_points": "Как да спечелиш точки за постижения?",
|
||||
"achievement_unlocked": "Постижението е отключено",
|
||||
"user_achievements": "Постиженията на {{displayName}} ",
|
||||
"your_achievements": "Вашите Постижения",
|
||||
"unlocked_at": "Отключено на: {{date}}",
|
||||
"subscription_needed": "Необходим е абонамент за Hydra Cloud, за да видите това съдържание",
|
||||
"new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игра",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения",
|
||||
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
|
||||
"how_to_earn_achievements_points": "Как се печелят точки от постижения?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Абонамент за Hydra Cloud",
|
||||
"subscribe_now": "Абонирай се сега",
|
||||
"cloud_saving": "Облачно запазване",
|
||||
"cloud_achievements": "Запазете постиженията си в облака",
|
||||
"animated_profile_picture": "Анимирани профилни снимки",
|
||||
"premium_support": "Премиум поддръжка",
|
||||
"show_and_compare_achievements": "Показвайте и сравнявайте постиженията си с други потребители",
|
||||
"animated_profile_banner": "Анимирани профилни банери",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Открихте функция на Hydra Cloud!",
|
||||
"learn_more": "Научете повече",
|
||||
"subscription_tour_title": "Hydra Cloud Абонамент",
|
||||
"subscribe_now": "Абонирай се сега",
|
||||
"cloud_saving": "Запазване в облака",
|
||||
"cloud_achievements": "Запазете постиженията си в облака",
|
||||
"animated_profile_picture": "Анимирана профилна снимка",
|
||||
"premium_support": "Премиум поддръжка",
|
||||
"show_and_compare_achievements": "Показвайте и сравнявайте постиженията си с тези на други потребители",
|
||||
"animated_profile_banner": "Анимиран профилен банер"
|
||||
"debrid_description": "Изтегляйте до 4 пъти по-бързо с Nimbus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
"home": {
|
||||
"featured": "Empfohlen",
|
||||
"surprise_me": "Überrasche mich",
|
||||
"no_results": "Keine Ergebnisse gefunden"
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"start_typing": "Tippe, um zu suchen...",
|
||||
"hot": "Jetzt beliebt",
|
||||
"weekly": "📅 Top-Spiele der Woche",
|
||||
"achievements": "🏆 Spiele zum Meistern"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
@@ -21,11 +25,13 @@
|
||||
"queued": "{{title}} (In Warteschlange)",
|
||||
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
||||
"sign_in": "Anmelden",
|
||||
"favorites": "Favoriten"
|
||||
"friends": "Freunde",
|
||||
"need_help": "Brauchst du Hilfe?",
|
||||
"favorites": "Favoriten",
|
||||
"playable_button_title": "Nur Spiele anzeigen, die du jetzt spielen kannst"
|
||||
},
|
||||
"header": {
|
||||
"search": "Spiele suchen",
|
||||
|
||||
"home": "Home",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
@@ -39,9 +45,21 @@
|
||||
"downloading_metadata": "Metadaten von {{title}} werden heruntergeladen…",
|
||||
"downloading": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Abschluss {{eta}} - {{speed}}",
|
||||
"calculating_eta": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Verbleibende Zeit wird berechnet…",
|
||||
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)"
|
||||
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Installation abgeschlossen",
|
||||
"installation_complete_message": "Allgemeine Redistributables erfolgreich installiert"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filtern…",
|
||||
"developers": "Entwickler",
|
||||
"genres": "Genres",
|
||||
"tags": "Tags",
|
||||
"publishers": "Publisher",
|
||||
"download_sources": "Download-Quellen",
|
||||
"result_count": "{{resultCount}} Ergebnisse",
|
||||
"filter_count": "{{filterCount}} verfügbar",
|
||||
"clear_filters": "{{filterCount}} ausgewählte löschen",
|
||||
"next_page": "Nächste Seite",
|
||||
"previous_page": "Vorherige Seite"
|
||||
},
|
||||
@@ -101,6 +119,7 @@
|
||||
"open_folder": "Verzeichnis öffnen",
|
||||
"open_download_location": "Heruntergeladene Dateien anzeigen",
|
||||
"create_shortcut": "Desktop-Verknüpfung erstellen",
|
||||
"clear": "Löschen",
|
||||
"remove_files": "Dateien entfernen",
|
||||
"remove_from_library_title": "Bist du dir sicher?",
|
||||
"remove_from_library_description": "Dies wird {{game}} aus deiner Bibliothek entfernen",
|
||||
@@ -114,8 +133,81 @@
|
||||
"download_in_progress": "Download erfolgt",
|
||||
"download_paused": "Download ist pausiert",
|
||||
"last_downloaded_option": "Letzte Download-Option",
|
||||
"create_steam_shortcut": "Steam-Verknüpfung erstellen",
|
||||
"create_shortcut_success": "Verknüpfung erfolgreich erstellt",
|
||||
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung"
|
||||
"you_might_need_to_restart_steam": "Möglicherweise musst du Steam neu starten, um die Änderungen zu sehen",
|
||||
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung",
|
||||
"nsfw_content_title": "Dieses Spiel enthält unangemessene Inhalte",
|
||||
"nsfw_content_description": "{{title}} enthält Inhalte, die möglicherweise nicht für alle Altersgruppen geeignet sind. Bist du sicher, dass du fortfahren möchtest?",
|
||||
"allow_nsfw_content": "Fortfahren",
|
||||
"refuse_nsfw_content": "Zurück",
|
||||
"stats": "Statistiken",
|
||||
"download_count": "Downloads",
|
||||
"player_count": "Aktive Spieler",
|
||||
"download_error": "Diese Download-Option ist nicht verfügbar",
|
||||
"download": "Download",
|
||||
"executable_path_in_use": "Ausführbare Datei wird bereits von \"{{game}}\" verwendet",
|
||||
"warning": "Warnung:",
|
||||
"hydra_needs_to_remain_open": "Für diesen Download muss Hydra geöffnet bleiben, bis er abgeschlossen ist. Wenn Hydra vor Abschluss geschlossen wird, verlierst du deinen Fortschritt.",
|
||||
"achievements": "Erfolge",
|
||||
"achievements_count": "Erfolge {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Cloud-Speicherstand",
|
||||
"cloud_save_description": "Speichere deinen Fortschritt in der Cloud und spiele auf jedem Gerät weiter",
|
||||
"backups": "Sicherungen",
|
||||
"install_backup": "Installieren",
|
||||
"delete_backup": "Löschen",
|
||||
"create_backup": "Neue Sicherung",
|
||||
"last_backup_date": "Letzte Sicherung am {{date}}",
|
||||
"no_backup_preview": "Keine Spielstände für diesen Titel gefunden",
|
||||
"restoring_backup": "Sicherung wird wiederhergestellt ({{progress}} abgeschlossen)…",
|
||||
"uploading_backup": "Sicherung wird hochgeladen…",
|
||||
"no_backups": "Du hast noch keine Sicherungen für dieses Spiel erstellt",
|
||||
"backup_uploaded": "Sicherung hochgeladen",
|
||||
"backup_deleted": "Sicherung gelöscht",
|
||||
"backup_restored": "Sicherung wiederhergestellt",
|
||||
"see_all_achievements": "Alle Erfolge anzeigen",
|
||||
"sign_in_to_see_achievements": "Anmelden, um Erfolge zu sehen",
|
||||
"mapping_method_automatic": "Automatisch",
|
||||
"mapping_method_manual": "Manuell",
|
||||
"mapping_method_label": "Zuordnungsmethode",
|
||||
"files_automatically_mapped": "Dateien automatisch zugeordnet",
|
||||
"no_backups_created": "Keine Sicherungen für dieses Spiel erstellt",
|
||||
"manage_files": "Dateien verwalten",
|
||||
"loading_save_preview": "Suche nach Spielständen…",
|
||||
"wine_prefix": "Wine-Präfix",
|
||||
"wine_prefix_description": "Das Wine-Präfix, das zum Ausführen dieses Spiels verwendet wird",
|
||||
"launch_options": "Startoptionen",
|
||||
"launch_options_description": "Fortgeschrittene Benutzer können Modifikationen ihrer Startoptionen eingeben (experimentelle Funktion)",
|
||||
"launch_options_placeholder": "Kein Parameter angegeben",
|
||||
"no_download_option_info": "Keine Informationen verfügbar",
|
||||
"backup_deletion_failed": "Sicherung konnte nicht gelöscht werden",
|
||||
"max_number_of_artifacts_reached": "Maximale Anzahl von Sicherungen für dieses Spiel erreicht",
|
||||
"achievements_not_sync": "Sieh, wie du deine Erfolge synchronisieren kannst",
|
||||
"manage_files_description": "Verwalte, welche Dateien gesichert und wiederhergestellt werden",
|
||||
"select_folder": "Ordner auswählen",
|
||||
"backup_from": "Sicherung vom {{date}}",
|
||||
"automatic_backup_from": "Automatische Sicherung vom {{date}}",
|
||||
"enable_automatic_cloud_sync": "Automatische Cloud-Synchronisierung aktivieren",
|
||||
"custom_backup_location_set": "Benutzerdefinierter Sicherungsort festgelegt",
|
||||
"no_directory_selected": "Kein Verzeichnis ausgewählt",
|
||||
"no_write_permission": "Kann nicht in dieses Verzeichnis herunterladen. Klicke hier, um mehr zu erfahren.",
|
||||
"reset_achievements": "Erfolge zurücksetzen",
|
||||
"reset_achievements_description": "Dies wird alle Erfolge für {{game}} zurücksetzen",
|
||||
"reset_achievements_title": "Bist du dir sicher?",
|
||||
"reset_achievements_success": "Erfolge erfolgreich zurückgesetzt",
|
||||
"reset_achievements_error": "Fehler beim Zurücksetzen der Erfolge",
|
||||
"download_error_gofile_quota_exceeded": "Du hast dein monatliches Gofile-Kontingent überschritten. Bitte warte, bis das Kontingent zurückgesetzt wird.",
|
||||
"download_error_real_debrid_account_not_authorized": "Dein Real-Debrid-Konto ist nicht für neue Downloads autorisiert. Bitte überprüfe deine Kontoeinstellungen und versuche es erneut.",
|
||||
"download_error_not_cached_on_real_debrid": "Dieser Download ist nicht auf Real-Debrid verfügbar und das Abrufen des Download-Status von Real-Debrid ist noch nicht verfügbar.",
|
||||
"download_error_not_cached_on_torbox": "Dieser Download ist nicht auf TorBox verfügbar und das Abrufen des Download-Status von TorBox ist noch nicht verfügbar.",
|
||||
"download_error_not_cached_on_hydra": "Dieser Download ist nicht auf Nimbus verfügbar.",
|
||||
"game_removed_from_favorites": "Spiel aus Favoriten entfernt",
|
||||
"game_added_to_favorites": "Spiel zu Favoriten hinzugefügt",
|
||||
"automatically_extract_downloaded_files": "Heruntergeladene Dateien automatisch entpacken",
|
||||
"create_start_menu_shortcut": "Startmenü-Verknüpfung erstellen",
|
||||
"invalid_wine_prefix_path": "Ungültiger Wine-Präfix-Pfad",
|
||||
"invalid_wine_prefix_path_description": "Der Pfad zum Wine-Präfix ist ungültig. Bitte überprüfe den Pfad und versuche es erneut.",
|
||||
"missing_wine_prefix": "Wine-Präfix ist erforderlich, um eine Sicherung unter Linux zu erstellen"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra aktivieren",
|
||||
@@ -148,7 +240,13 @@
|
||||
"queued": "In Warteschlange",
|
||||
"no_downloads_title": "Welch Leere",
|
||||
"no_downloads_description": "Du hast mit Hydra noch nichts heruntergeladen, aber es ist nie zu spät anzufangen.",
|
||||
"checking_files": "Dateien werden überprüft…"
|
||||
"checking_files": "Dateien werden überprüft…",
|
||||
"seeding": "Seeding",
|
||||
"stop_seeding": "Seeding stoppen",
|
||||
"resume_seeding": "Seeding fortsetzen",
|
||||
"options": "Verwalten",
|
||||
"extract": "Dateien entpacken",
|
||||
"extracting": "Dateien werden entpackt…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Download-Pfad",
|
||||
@@ -185,11 +283,11 @@
|
||||
"download_source_errored": "Fehlgeschlagen",
|
||||
"sync_download_sources": "Quellen synchronisieren",
|
||||
"removed_download_source": "Download-Quelle entfernt",
|
||||
"removed_download_sources": "Download-Quellen entfernt",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nein",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, alles löschen",
|
||||
"description_confirmation_delete_all_sources": "Du löschen alle Downloadquellen",
|
||||
"title_confirmation_delete_all_sources": "Löschen du alle Downloadquellen",
|
||||
"removed_download_sources": "Download-Quellen entfernt",
|
||||
"title_confirmation_delete_all_sources": "Möchtest du alle Downloadquellen löschen",
|
||||
"description_confirmation_delete_all_sources": "Möchtest du alle Downloadquellen löschen",
|
||||
"button_delete_all_sources": "Entfernen Sie alle Downloadquellen",
|
||||
"added_download_source": "Download-Quelle hinzugefügt",
|
||||
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
|
||||
@@ -197,7 +295,95 @@
|
||||
"found_download_option_zero": "Keine Download-Option gefunden",
|
||||
"found_download_option_one": "{{countFormatted}} Download-Option gefunden",
|
||||
"found_download_option_other": "{{countFormatted}} Download-Optionen gefunden",
|
||||
"import": "Importieren"
|
||||
"import": "Importieren",
|
||||
"public": "Öffentlich",
|
||||
"private": "Privat",
|
||||
"friends_only": "Nur Freunde",
|
||||
"privacy": "Privatsphäre",
|
||||
"profile_visibility": "Profilsichtbarkeit",
|
||||
"profile_visibility_description": "Wähle, wer dein Profil und deine Bibliothek sehen kann",
|
||||
"required_field": "Dieses Feld ist erforderlich",
|
||||
"source_already_exists": "Diese Quelle wurde bereits hinzugefügt",
|
||||
"must_be_valid_url": "Die Quelle muss eine gültige URL sein",
|
||||
"blocked_users": "Blockierte Benutzer",
|
||||
"user_unblocked": "Benutzer wurde freigegeben",
|
||||
"enable_achievement_notifications": "Wenn ein Erfolg freigeschaltet wird",
|
||||
"launch_minimized": "Hydra minimiert starten",
|
||||
"disable_nsfw_alert": "NSFW-Warnung deaktivieren",
|
||||
"seed_after_download_complete": "Nach Download-Abschluss seeden",
|
||||
"show_hidden_achievement_description": "Versteckte Erfolgsbeschreibungen vor dem Freischalten anzeigen",
|
||||
"account": "Konto",
|
||||
"no_users_blocked": "Du hast keine blockierten Benutzer",
|
||||
"subscription_active_until": "Deine Hydra Cloud ist aktiv bis {{date}}",
|
||||
"manage_subscription": "Abonnement verwalten",
|
||||
"update_email": "E-Mail aktualisieren",
|
||||
"update_password": "Passwort aktualisieren",
|
||||
"current_email": "Aktuelle E-Mail:",
|
||||
"no_email_account": "Du hast noch keine E-Mail festgelegt",
|
||||
"account_data_updated_successfully": "Kontodaten erfolgreich aktualisiert",
|
||||
"renew_subscription": "Hydra Cloud erneuern",
|
||||
"subscription_expired_at": "Dein Abonnement ist am {{date}} abgelaufen",
|
||||
"no_subscription": "Genieße Hydra auf die bestmögliche Weise",
|
||||
"become_subscriber": "Werde Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Automatische Verlängerung ist deaktiviert",
|
||||
"subscription_renews_on": "Dein Abonnement verlängert sich am {{date}}",
|
||||
"bill_sent_until": "Deine nächste Rechnung wird bis zu diesem Tag gesendet",
|
||||
"no_themes": "Scheint, als hättest du noch keine Themes, aber keine Sorge, klicke hier, um dein erstes Meisterwerk zu erstellen.",
|
||||
"editor_tab_code": "Code",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Speichern",
|
||||
"web_store": "Web Store",
|
||||
"clear_themes": "Löschen",
|
||||
"create_theme": "Erstellen",
|
||||
"create_theme_modal_title": "Benutzerdefiniertes Theme erstellen",
|
||||
"create_theme_modal_description": "Erstelle ein neues Theme, um das Aussehen von Hydra anzupassen",
|
||||
"theme_name": "Name",
|
||||
"insert_theme_name": "Theme-Namen eingeben",
|
||||
"set_theme": "Theme festlegen",
|
||||
"unset_theme": "Theme entfernen",
|
||||
"delete_theme": "Theme löschen",
|
||||
"edit_theme": "Theme bearbeiten",
|
||||
"delete_all_themes": "Alle Themes löschen",
|
||||
"delete_all_themes_description": "Dies wird alle deine benutzerdefinierten Themes löschen",
|
||||
"delete_theme_description": "Dies wird das Theme {{theme}} löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"appearance": "Erscheinungsbild",
|
||||
"enable_torbox": "TorBox aktivieren",
|
||||
"torbox_description": "TorBox ist dein Premium-Seedbox-Service, der sogar mit den besten Servern auf dem Markt konkurriert.",
|
||||
"torbox_account_linked": "TorBox-Konto verknüpft",
|
||||
"create_real_debrid_account": "Klicke hier, wenn du noch kein Real-Debrid-Konto hast",
|
||||
"create_torbox_account": "Klicke hier, wenn du noch kein TorBox-Konto hast",
|
||||
"real_debrid_account_linked": "Real-Debrid-Konto verknüpft",
|
||||
"name_min_length": "Theme-Name muss mindestens 3 Zeichen lang sein",
|
||||
"import_theme": "Theme importieren",
|
||||
"import_theme_description": "Du wirst {{theme}} aus dem Theme Store importieren",
|
||||
"error_importing_theme": "Fehler beim Importieren des Themes",
|
||||
"theme_imported": "Theme erfolgreich importiert",
|
||||
"enable_friend_request_notifications": "Wenn eine Freundschaftsanfrage empfangen wird",
|
||||
"enable_auto_install": "Updates automatisch herunterladen",
|
||||
"common_redist": "Allgemeine Redistributables",
|
||||
"common_redist_description": "Allgemeine Redistributables sind erforderlich, um einige Spiele auszuführen. Es wird empfohlen, sie zu installieren, um Probleme zu vermeiden.",
|
||||
"install_common_redist": "Installieren",
|
||||
"installing_common_redist": "Installiere…",
|
||||
"show_download_speed_in_megabytes": "Download-Geschwindigkeit in Megabyte pro Sekunde anzeigen",
|
||||
"extract_files_by_default": "Dateien nach dem Download standardmäßig entpacken",
|
||||
"achievement_custom_notification_position": "Position der benutzerdefinierten Erfolgsbenachrichtigung",
|
||||
"top-left": "Oben links",
|
||||
"top-center": "Oben mittig",
|
||||
"top-right": "Oben rechts",
|
||||
"bottom-left": "Unten links",
|
||||
"bottom-center": "Unten mittig",
|
||||
"bottom-right": "Unten rechts",
|
||||
"enable_achievement_custom_notifications": "Benutzerdefinierte Erfolgsbenachrichtigungen aktivieren",
|
||||
"alignment": "Ausrichtung",
|
||||
"variation": "Variation",
|
||||
"default": "Standard",
|
||||
"rare": "Selten",
|
||||
"platinum": "Platin",
|
||||
"hidden": "Versteckt",
|
||||
"test_notification": "Testbenachrichtigung",
|
||||
"notification_preview": "Vorschau der Erfolgsbenachrichtigung",
|
||||
"enable_friend_start_game_notifications": "Wenn ein Freund ein Spiel startet"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download abgeschlossen",
|
||||
@@ -206,13 +392,24 @@
|
||||
"repack_count_one": "{{count}} Repack hinzugefügt",
|
||||
"repack_count_other": "{{count}} Repacks hinzugefügt",
|
||||
"new_update_available": "Version {{version}} verfügbar",
|
||||
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu"
|
||||
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu",
|
||||
"notification_achievement_unlocked_title": "Erfolg für {{game}} freigeschaltet",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} und {{count}} weitere wurden freigeschaltet",
|
||||
"new_friend_request_description": "{{displayName}} hat dir eine Freundschaftsanfrage gesendet",
|
||||
"new_friend_request_title": "Neue Freundschaftsanfrage",
|
||||
"extraction_complete": "Entpacken abgeschlossen",
|
||||
"game_extracted": "{{title}} erfolgreich entpackt",
|
||||
"friend_started_playing_game": "{{displayName}} hat begonnen, ein Spiel zu spielen",
|
||||
"test_achievement_notification_title": "Dies ist eine Testbenachrichtigung",
|
||||
"test_achievement_notification_description": "Ziemlich cool, oder?"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra öffnen",
|
||||
"quit": "Schließen"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Verfügbar",
|
||||
"available_other": "Verfügbar",
|
||||
"no_downloads": "Keine Downloads verfügbar"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -274,6 +471,66 @@
|
||||
"no_pending_invites": "Du hast keine ausstehenden Einladungen",
|
||||
"no_blocked_users": "Du hast keine blockierten Nutzer",
|
||||
"friend_code_copied": "Freundescode kopiert",
|
||||
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt"
|
||||
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt",
|
||||
"privacy_hint": "Um anzupassen, wer dies sehen kann, gehe zu den <0>Einstellungen</0>",
|
||||
"locked_profile": "Dieses Profil ist privat",
|
||||
"image_process_failure": "Fehler bei der Bildverarbeitung",
|
||||
"required_field": "Dieses Feld ist erforderlich",
|
||||
"displayname_min_length": "Anzeigename muss mindestens 3 Zeichen lang sein",
|
||||
"displayname_max_length": "Anzeigename darf maximal 50 Zeichen lang sein",
|
||||
"report_profile": "Dieses Profil melden",
|
||||
"report_reason": "Warum meldest du dieses Profil?",
|
||||
"report_description": "Zusätzliche Informationen",
|
||||
"report_description_placeholder": "Zusätzliche Informationen",
|
||||
"report": "Melden",
|
||||
"report_reason_hate": "Hassrede",
|
||||
"report_reason_sexual_content": "Sexuelle Inhalte",
|
||||
"report_reason_violence": "Gewalt",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Sonstiges",
|
||||
"profile_reported": "Profil gemeldet",
|
||||
"your_friend_code": "Dein Freundescode:",
|
||||
"upload_banner": "Banner hochladen",
|
||||
"uploading_banner": "Banner wird hochgeladen…",
|
||||
"background_image_updated": "Hintergrundbild aktualisiert",
|
||||
"stats": "Statistiken",
|
||||
"achievements": "Erfolge",
|
||||
"games": "Spiele",
|
||||
"top_percentile": "Top {{percentile}}%",
|
||||
"ranking_updated_weekly": "Rangliste wird wöchentlich aktualisiert",
|
||||
"playing": "Spielt {{game}}",
|
||||
"achievements_unlocked": "Erfolge freigeschaltet",
|
||||
"earned_points": "Verdiente Punkte",
|
||||
"show_achievements_on_profile": "Zeige deine Erfolge auf deinem Profil",
|
||||
"show_points_on_profile": "Zeige deine verdienten Punkte auf deinem Profil"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Erfolg freigeschaltet",
|
||||
"user_achievements": "{{displayName}}'s Erfolge",
|
||||
"your_achievements": "Deine Erfolge",
|
||||
"unlocked_at": "Freigeschaltet am: {{date}}",
|
||||
"subscription_needed": "Ein Hydra Cloud-Abonnement ist erforderlich, um diesen Inhalt zu sehen",
|
||||
"new_achievements_unlocked": "{{achievementCount}} neue Erfolge von {{gameCount}} Spielen freigeschaltet",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} Erfolge",
|
||||
"achievements_unlocked_for_game": "{{achievementCount}} neue Erfolge für {{gameTitle}} freigeschaltet",
|
||||
"hidden_achievement_tooltip": "Dies ist ein versteckter Erfolg",
|
||||
"achievement_earn_points": "Verdiene {{points}} Punkte mit diesem Erfolg",
|
||||
"earned_points": "Verdiente Punkte:",
|
||||
"available_points": "Verfügbare Punkte:",
|
||||
"how_to_earn_achievements_points": "Wie verdient man Erfolgspunkte?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud-Abonnement",
|
||||
"subscribe_now": "Jetzt abonnieren",
|
||||
"cloud_saving": "Cloud-Speicherung",
|
||||
"cloud_achievements": "Speichere deine Erfolge in der Cloud",
|
||||
"animated_profile_picture": "Animierte Profilbilder",
|
||||
"premium_support": "Premium-Support",
|
||||
"show_and_compare_achievements": "Zeige und vergleiche deine Erfolge mit anderen Nutzern",
|
||||
"animated_profile_banner": "Animiertes Profilbanner",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Du hast gerade eine Hydra Cloud-Funktion entdeckt!",
|
||||
"learn_more": "Mehr erfahren",
|
||||
"debrid_description": "Lade bis zu 4x schneller mit Nimbus herunter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"sign_in": "Sign in",
|
||||
"friends": "Friends",
|
||||
"need_help": "Need help?",
|
||||
"favorites": "Favorites"
|
||||
"favorites": "Favorites",
|
||||
"playable_button_title": "Show only games you can play now"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
@@ -88,6 +89,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}}",
|
||||
@@ -130,9 +132,11 @@
|
||||
"download_in_progress": "Download in progress",
|
||||
"download_paused": "Download paused",
|
||||
"last_downloaded_option": "Last downloaded option",
|
||||
"create_steam_shortcut": "Create Steam shortcut",
|
||||
"create_shortcut_success": "Shortcut created successfully",
|
||||
"you_might_need_to_restart_steam": "You might need to restart Steam to see the changes",
|
||||
"create_shortcut_error": "Error creating shortcut",
|
||||
"nsfw_content_title": "This game contains innapropriate content",
|
||||
"nsfw_content_title": "This game contains inappropriate content",
|
||||
"nsfw_content_description": "{{title}} contains content that may not be suitable for all ages. Are you sure you want to continue?",
|
||||
"allow_nsfw_content": "Continue",
|
||||
"refuse_nsfw_content": "Go back",
|
||||
@@ -194,12 +198,39 @@
|
||||
"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"
|
||||
"create_start_menu_shortcut": "Create Start Menu shortcut",
|
||||
"invalid_wine_prefix_path": "Invalid Wine prefix path",
|
||||
"invalid_wine_prefix_path_description": "The path to the Wine prefix is invalid. Please check the path and try again.",
|
||||
"missing_wine_prefix": "Wine prefix is required to create a backup on Linux",
|
||||
"artifact_renamed": "Backup renamed successfully",
|
||||
"rename_artifact": "Rename Backup",
|
||||
"rename_artifact_description": "Rename the backup to a more descriptive name",
|
||||
"artifact_name_label": "Backup name",
|
||||
"artifact_name_placeholder": "Enter a name for the backup",
|
||||
"save_changes": "Save changes",
|
||||
"required_field": "This field is required",
|
||||
"max_length_field": "This field must be less than {{length}} characters",
|
||||
"freeze_backup": "Pin it so it's not overwritten by automatic backups",
|
||||
"unfreeze_backup": "Unpin it",
|
||||
"backup_frozen": "Backup pinned",
|
||||
"backup_unfrozen": "Backup unpinned",
|
||||
"backup_freeze_failed": "Failed to freeze backup",
|
||||
"backup_freeze_failed_description": "You must leave at least one free slot for automatic backups"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@@ -358,7 +389,25 @@
|
||||
"install_common_redist": "Install",
|
||||
"installing_common_redist": "Installing…",
|
||||
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
|
||||
"extract_files_by_default": "Extract files by default after download"
|
||||
"extract_files_by_default": "Extract files by default after download",
|
||||
"enable_steam_achievements": "Enable search for Steam achievements",
|
||||
"achievement_custom_notification_position": "Achievement custom notification position",
|
||||
"top-left": "Top left",
|
||||
"top-center": "Top center",
|
||||
"top-right": "Top right",
|
||||
"bottom-left": "Bottom left",
|
||||
"bottom-center": "Bottom center",
|
||||
"bottom-right": "Bottom right",
|
||||
"enable_achievement_custom_notifications": "Enable achievement custom notifications",
|
||||
"alignment": "Alignment",
|
||||
"variation": "Variation",
|
||||
"default": "Default",
|
||||
"rare": "Rare",
|
||||
"platinum": "Platinum",
|
||||
"hidden": "Hidden",
|
||||
"test_notification": "Test notification",
|
||||
"notification_preview": "Achievement Notification Preview",
|
||||
"enable_friend_start_game_notifications": "When a friend starts playing a game"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
@@ -374,7 +423,9 @@
|
||||
"new_friend_request_title": "New friend request",
|
||||
"extraction_complete": "Extraction complete",
|
||||
"game_extracted": "{{title}} extracted successfully",
|
||||
"friend_started_playing_game": "{{displayName}} started playing a game"
|
||||
"friend_started_playing_game": "{{displayName}} started playing a game",
|
||||
"test_achievement_notification_title": "This is a test notification",
|
||||
"test_achievement_notification_description": "Pretty cool, huh?"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Open Hydra",
|
||||
@@ -402,7 +453,9 @@
|
||||
"last_time_played": "Last played {{period}}",
|
||||
"activity": "Recent Activity",
|
||||
"library": "Library",
|
||||
"pinned": "Pinned",
|
||||
"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",
|
||||
@@ -475,7 +528,9 @@
|
||||
"achievements_unlocked": "Achievements Unlocked",
|
||||
"earned_points": "Earned points",
|
||||
"show_achievements_on_profile": "Show your achievements on your profile",
|
||||
"show_points_on_profile": "Show your earned points on your profile"
|
||||
"show_points_on_profile": "Show your earned points on your profile",
|
||||
"error_adding_friend": "Could not send friend request. Please check friend code",
|
||||
"friend_code_length_error": "Friend code must have 8 characters"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Achievement unlocked",
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"sign_in": "Iniciar sesión",
|
||||
"friends": "Amigos",
|
||||
"need_help": "¿Necesitas ayuda?",
|
||||
"favorites": "Favoritos"
|
||||
"favorites": "Favoritos",
|
||||
"playable_button_title": "Mostrar solo juegos que puedes jugar ahora"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar juegos",
|
||||
@@ -130,15 +131,17 @@
|
||||
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)",
|
||||
"download_in_progress": "Descarga en progreso",
|
||||
"download_paused": "Descarga pausada",
|
||||
"create_steam_shortcut": "Crear atajo de Steam",
|
||||
"last_downloaded_option": "Última opción descargada",
|
||||
"create_shortcut_success": "Atajo creado con éxito",
|
||||
"you_might_need_to_restart_steam": "Es posible que necesites reiniciar Steam para ver los cambios",
|
||||
"create_shortcut_error": "Error al crear un atajo",
|
||||
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
||||
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
|
||||
"allow_nsfw_content": "Continuar",
|
||||
"refuse_nsfw_content": "No, gracias",
|
||||
"stats": "Estadísticas",
|
||||
"download_count": "Downloads",
|
||||
"download_count": "Descargas",
|
||||
"player_count": "Jugadores activos",
|
||||
"download_error": "Esta opción de descarga no está disponible.",
|
||||
"download": "Descargar",
|
||||
@@ -196,9 +199,12 @@
|
||||
"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.",
|
||||
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y aún no se puede verificar el estado de la descarga.",
|
||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||
"game_removed_from_favorites": "Juego removido de favoritos"
|
||||
"game_removed_from_favorites": "Juego removido de favoritos",
|
||||
"invalid_wine_prefix_path": "Ruta de prefijo de Wine inválida",
|
||||
"invalid_wine_prefix_path_description": "La ruta del prefijo Wine es inválida. Por favor, checa la ruta y vuelve a intentarlo.",
|
||||
"missing_wine_prefix": "Se requiere el prefijo Wine para crear una copia de seguridad en Linux"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activar Hydra",
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"sign_in": "Se connecter",
|
||||
"friends": "Amis",
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"favorites": "Favoris"
|
||||
"favorites": "Favoris",
|
||||
"playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant"
|
||||
},
|
||||
"header": {
|
||||
"search": "Rechercher",
|
||||
@@ -356,7 +357,17 @@
|
||||
"common_redist_description": "Certains jeux nécessitent les redistribuables communs. L'installation est recommandée.",
|
||||
"install_common_redist": "Installer",
|
||||
"installing_common_redist": "Installation…",
|
||||
"show_download_speed_in_megabytes": "Afficher la vitesse de téléchargement en mégaoctets par seconde"
|
||||
"show_download_speed_in_megabytes": "Afficher la vitesse de téléchargement en mégaoctets par seconde",
|
||||
"extract_files_by_default": "Extraire les fichiers par défaut après le téléchargement",
|
||||
"enable_achievement_custom_notifications": "Activer les notifications personnalisées de succès",
|
||||
"achievement_custom_notification_position": "Position de la notification personnalisée de succès",
|
||||
"top-left": "En haut à gauche",
|
||||
"top-center": "En haut au centre",
|
||||
"top-right": "En haut à droite",
|
||||
"bottom-left": "En bas à gauche",
|
||||
"bottom-center": "En bas au centre",
|
||||
"bottom-right": "En bas à droite",
|
||||
"enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Téléchargement terminé",
|
||||
|
||||
@@ -26,6 +26,7 @@ import nb from "./nb/translation.json";
|
||||
import et from "./et/translation.json";
|
||||
import bg from "./bg/translation.json";
|
||||
import uz from "./uz/translation.json";
|
||||
import sv from "./sv/translation.json";
|
||||
|
||||
export default {
|
||||
"pt-BR": ptBR,
|
||||
@@ -56,4 +57,5 @@ export default {
|
||||
nb,
|
||||
et,
|
||||
uz,
|
||||
sv,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||
"filter": "Filtra libreria",
|
||||
"home": "Home",
|
||||
"favorites": "Preferiti"
|
||||
"favorites": "Preferiti",
|
||||
"playable_button_title": "Mostra solo i giochi che puoi giocare ora"
|
||||
},
|
||||
"header": {
|
||||
"search": "Cerca",
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
"downloading_metadata": "{{title}} (Pobieranie metadata…)",
|
||||
"paused": "{{title}} (Zatrzymano)",
|
||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||
"filter": "Filtruj biblioteke",
|
||||
"filter": "Filtruj bibliotekę",
|
||||
"home": "Główna",
|
||||
"favorites": "Ulubione"
|
||||
"favorites": "Ulubione",
|
||||
"playable_button_title": "Pokaż tylko gry, w które możesz grać teraz"
|
||||
},
|
||||
"header": {
|
||||
"search": "Szukaj",
|
||||
|
||||
@@ -76,6 +76,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}}",
|
||||
@@ -118,7 +119,9 @@
|
||||
"download_in_progress": "Download em andamento",
|
||||
"download_paused": "Download pausado",
|
||||
"last_downloaded_option": "Última opção baixada",
|
||||
"create_steam_shortcut": "Criar atalho na Steam",
|
||||
"create_shortcut_success": "Atalho criado com sucesso",
|
||||
"you_might_need_to_restart_steam": "Você pode precisar reiniciar a Steam para ver as alterações",
|
||||
"create_shortcut_error": "Erro ao criar atalho",
|
||||
"nsfw_content_title": "Este jogo contém conteúdo inapropriado",
|
||||
"nsfw_content_description": "{{title}} contém conteúdo que pode não ser apropriado para todas as idades. Você deseja continuar?",
|
||||
@@ -188,7 +191,23 @@
|
||||
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos",
|
||||
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados",
|
||||
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar"
|
||||
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
|
||||
"invalid_wine_prefix_path": "Caminho do prefixo Wine inválido",
|
||||
"invalid_wine_prefix_path_description": "O caminho para o prefixo Wine é inválido. Por favor, verifique o caminho e tente novamente.",
|
||||
"artifact_renamed": "Backup renomeado com sucesso",
|
||||
"rename_artifact": "Renomear Backup",
|
||||
"rename_artifact_description": "Renomeie o backup para um nome mais descritivo",
|
||||
"artifact_name_label": "Nome do backup",
|
||||
"artifact_name_placeholder": "Insira um nome para o backup",
|
||||
"save_changes": "Salvar mudanças",
|
||||
"required_field": "Este campo é obrigatório",
|
||||
"max_length_field": "Este campo deve ter menos de {{length}} caracteres",
|
||||
"freeze_backup": "Fixar para não ser apagado por backups automáticos",
|
||||
"unfreeze_backup": "Remover dos fixados",
|
||||
"backup_frozen": "Backup fixado",
|
||||
"backup_unfrozen": "Backup removido dos fixados",
|
||||
"backup_freeze_failed": "Falha ao fixar backup",
|
||||
"backup_freeze_failed_description": "Você deve deixar pelo menos um espaço livre para backups automáticos"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -345,7 +364,25 @@
|
||||
"install_common_redist": "Instalar",
|
||||
"installing_common_redist": "Instalando…",
|
||||
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
|
||||
"extract_files_by_default": "Extrair arquivos automaticamente após o download"
|
||||
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
|
||||
"enable_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",
|
||||
"top-right": "Superior direito",
|
||||
"bottom-left": "Inferior esquerdo",
|
||||
"bottom-right": "Inferior direito",
|
||||
"bottom-center": "Inferior central",
|
||||
"achievement_custom_notification_position": "Posição das notificações customizadas de conquista",
|
||||
"alignment": "Alinhamento",
|
||||
"variation": "Variação",
|
||||
"default": "Padrão",
|
||||
"rare": "Rara",
|
||||
"platinum": "Platina",
|
||||
"hidden": "Oculta",
|
||||
"test_notification": "Testar notificação",
|
||||
"notification_preview": "Prévia da Notificação de Conquistas",
|
||||
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
@@ -359,7 +396,9 @@
|
||||
"new_friend_request_description": "{{displayName}} te enviou um pedido de amizade",
|
||||
"extraction_complete": "Extração concluída",
|
||||
"game_extracted": "{{title}} extraído com sucesso",
|
||||
"friend_started_playing_game": "{{displayName}} começou a jogar"
|
||||
"friend_started_playing_game": "{{displayName}} começou a jogar",
|
||||
"test_achievement_notification_title": "Esta é uma notificação de teste",
|
||||
"test_achievement_notification_description": "Bem legal, né?"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
@@ -470,7 +509,9 @@
|
||||
"achievements_unlocked": "Conquistas desbloqueadas",
|
||||
"earned_points": "Pontos ganhos",
|
||||
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
|
||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
|
||||
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
|
||||
"friend_code_length_error": "Código de amigo deve ter 8 caracteres"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Conquista desbloqueada",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"hot": "Сейчас популярно",
|
||||
"start_typing": "Начинаю вводить текст...",
|
||||
"weekly": "📅 Лучшие игры недели",
|
||||
"achievements": "🏆 Игры с достижениями"
|
||||
"achievements": "🏆 Игры с достижениями",
|
||||
"already_in_library": "Уже в библиотеке"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Каталог",
|
||||
@@ -27,7 +28,8 @@
|
||||
"sign_in": "Войти",
|
||||
"friends": "Друзья",
|
||||
"need_help": "Нужна помощь?",
|
||||
"favorites": "Избранное"
|
||||
"favorites": "Избранное",
|
||||
"playable_button_title": "Показать только игры, в которые можно играть сейчас"
|
||||
},
|
||||
"header": {
|
||||
"search": "Поиск",
|
||||
@@ -194,10 +196,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 +384,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 +416,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",
|
||||
@@ -399,6 +450,7 @@
|
||||
"activity": "Недавняя активность",
|
||||
"library": "Библиотека",
|
||||
"total_play_time": "Всего сыграно",
|
||||
"manual_playtime_tooltip": "Время игры было обновлено вручную",
|
||||
"no_recent_activity_title": "Хммм... Тут ничего нет",
|
||||
"no_recent_activity_description": "Вы давно ни во что не играли. Пора это изменить!",
|
||||
"display_name": "Отображаемое имя",
|
||||
@@ -471,7 +523,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": "Достижение разблокировано",
|
||||
|
||||
533
src/locales/sv/translation.json
Normal file
533
src/locales/sv/translation.json
Normal file
@@ -0,0 +1,533 @@
|
||||
{
|
||||
"language_name": "Svenska",
|
||||
"app": {
|
||||
"successfully_signed_in": "Inloggningen lyckades"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Utvalt",
|
||||
"surprise_me": "Överraska mig",
|
||||
"no_results": "Inga resultat hittades",
|
||||
"start_typing": "Börja skriva för att söka...",
|
||||
"hot": "Hetast just nu",
|
||||
"weekly": "📅 Veckans topplista",
|
||||
"achievements": "🏆 Spel att klara av"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Nedladdningar",
|
||||
"settings": "Inställningar",
|
||||
"my_library": "Mitt bibliotek",
|
||||
"downloading_metadata": "{{title}} (Hämtar metadata…)",
|
||||
"paused": "{{title}} (Pausad)",
|
||||
"downloading": "{{title}} ({{percentage}} - Hämtar…)",
|
||||
"filter": "Filtrera bibliotek",
|
||||
"home": "Hem",
|
||||
"queued": "{{title}} (I kö)",
|
||||
"game_has_no_executable": "Spelet har ingen vald körbar fil",
|
||||
"sign_in": "Logga in",
|
||||
"friends": "Vänner",
|
||||
"need_help": "Behöver du hjälp?",
|
||||
"favorites": "Favoriter"
|
||||
},
|
||||
"header": {
|
||||
"search": "Sök spel",
|
||||
"home": "Hem",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Nedladdningar",
|
||||
"search_results": "Sökresultat",
|
||||
"settings": "Inställningar",
|
||||
"version_available_install": "Version {{version}} är tillgänglig. Klicka här för att starta om och installera.",
|
||||
"version_available_download": "Version {{version}} är tillgänglig. Klicka här för att ladda ner."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Inga nedladdningar pågår",
|
||||
"downloading_metadata": "Laddar ner metadata för {{title}}…",
|
||||
"downloading": "Laddar ner {{title}}… ({{percentage}} klart) - Klart om {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Laddar ner {{title}}… ({{percentage}} klart) - Beräknar återstående tid…",
|
||||
"checking_files": "Kontrollerar filer för {{title}}… ({{percentage}} klart)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Installation klar",
|
||||
"installation_complete_message": "Nödvändiga systemkomponenter installerade framgångsrikt"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filter…",
|
||||
"developers": "Utvecklare",
|
||||
"genres": "Genrer",
|
||||
"tags": "Taggar",
|
||||
"publishers": "Utgivare",
|
||||
"download_sources": "Nedladdningskällor",
|
||||
"result_count": "{{resultCount}} resultat",
|
||||
"filter_count": "{{filterCount}} tillgängliga",
|
||||
"clear_filters": "Rensa {{filterCount}} valda"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Öppna nedladdningsalternativ",
|
||||
"download_options_zero": "Inget nedladdningsalternativ",
|
||||
"download_options_one": "{{count}} nedladdningsalternativ",
|
||||
"download_options_other": "{{count}} nedladdningsalternativ",
|
||||
"updated_at": "Uppdaterad {{updated_at}}",
|
||||
"install": "Installera",
|
||||
"resume": "Återuppta",
|
||||
"pause": "Pausa",
|
||||
"cancel": "Avbryt",
|
||||
"remove": "Ta bort",
|
||||
"space_left_on_disk": "{{space}} ledigt på disken",
|
||||
"eta": "Klart om {{eta}}",
|
||||
"calculating_eta": "Beräknar återstående tid…",
|
||||
"downloading_metadata": "Laddar ner metadata…",
|
||||
"filter": "Filtrera repacks",
|
||||
"requirements": "Systemkrav",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Rekommenderat",
|
||||
"paused": "Pausat",
|
||||
"release_date": "Släpptes den {{date}}",
|
||||
"publisher": "Utgiven av {{publisher}}",
|
||||
"hours": "timmar",
|
||||
"minutes": "minuter",
|
||||
"amount_hours": "{{amount}} timmar",
|
||||
"amount_minutes": "{{amount}} minuter",
|
||||
"accuracy": "{{accuracy}}% träffsäkerhet",
|
||||
"add_to_library": "Lägg till i biblioteket",
|
||||
"remove_from_library": "Ta bort från biblioteket",
|
||||
"no_downloads": "Inga nedladdningar tillgängliga",
|
||||
"play_time": "Spelad i {{amount}}",
|
||||
"last_time_played": "Senast spelad {{period}}",
|
||||
"not_played_yet": "Du har inte spelat {{title}} än",
|
||||
"next_suggestion": "Nästa förslag",
|
||||
"play": "Spela",
|
||||
"deleting": "Tar bort installationsfil…",
|
||||
"close": "Stäng",
|
||||
"playing_now": "Spelar nu",
|
||||
"change": "Byt",
|
||||
"repacks_modal_description": "Välj den repack du vill ladda ner",
|
||||
"select_folder_hint": "För att ändra standardmappen, gå till <0>Inställningar</0>",
|
||||
"download_now": "Ladda ner nu",
|
||||
"no_shop_details": "Kunde inte hämta butikens information.",
|
||||
"download_options": "Nedladdningsalternativ",
|
||||
"download_path": "Nedladdningsplats",
|
||||
"previous_screenshot": "Föregående skärmdump",
|
||||
"next_screenshot": "Nästa skärmdump",
|
||||
"screenshot": "Skärmdump {{number}}",
|
||||
"open_screenshot": "Öppna skärmdump {{number}}",
|
||||
"download_settings": "Nedladdningsinställningar",
|
||||
"downloader": "Nedladdare",
|
||||
"select_executable": "Välj",
|
||||
"no_executable_selected": "Ingen körbar fil vald",
|
||||
"open_folder": "Öppna mapp",
|
||||
"open_download_location": "Visa nedladdade filer",
|
||||
"create_shortcut": "Skapa genväg på skrivbordet",
|
||||
"clear": "Rensa",
|
||||
"remove_files": "Ta bort filer",
|
||||
"remove_from_library_title": "Är du säker?",
|
||||
"remove_from_library_description": "Detta kommer ta bort {{game}} från ditt bibliotek",
|
||||
"options": "Alternativ",
|
||||
"executable_section_title": "Körbar fil",
|
||||
"executable_section_description": "Sökväg till filen som körs när du klickar på \"Spela\"",
|
||||
"downloads_section_title": "Nedladdningar",
|
||||
"downloads_section_description": "Kolla uppdateringar eller andra versioner av detta spel",
|
||||
"danger_zone_section_title": "Danger zone",
|
||||
"danger_zone_section_description": "Ta bort detta spel från ditt bibliotek eller filer nedladdade av Hydra",
|
||||
"download_in_progress": "Nedladdning pågår",
|
||||
"download_paused": "Nedladdning pausad",
|
||||
"last_downloaded_option": "Senast nedladdade alternativ",
|
||||
"create_steam_shortcut": "Skapa Steam-genväg",
|
||||
"create_shortcut_success": "Genväg skapad",
|
||||
"you_might_need_to_restart_steam": "Du kan behöva starta om Steam för att se ändringarna",
|
||||
"create_shortcut_error": "Fel vid skapande av genväg",
|
||||
"nsfw_content_title": "Det här spelet innehåller olämpligt innehåll",
|
||||
"nsfw_content_description": "{{title}} innehåller innehåll som kanske inte är lämpligt för alla åldrar. Vill du fortsätta?",
|
||||
"allow_nsfw_content": "Fortsätt",
|
||||
"refuse_nsfw_content": "Gå tillbaka",
|
||||
"stats": "Statistik",
|
||||
"download_count": "Nedladdningar",
|
||||
"player_count": "Aktiva spelare",
|
||||
"download_error": "Det här nedladdningsalternativet är inte tillgängligt",
|
||||
"download": "Ladda ner",
|
||||
"executable_path_in_use": "Körbar fil används redan av \"{{game}}\"",
|
||||
"warning": "Varning:",
|
||||
"hydra_needs_to_remain_open": "för denna nedladdning behöver Hydra vara öppen tills den är klar. Om Hydra stängs innan nedladdningen är klar förlorar du dina framsteg.",
|
||||
"achievements": "Prestationer",
|
||||
"achievements_count": "Prestationer {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Molnspara",
|
||||
"cloud_save_description": "Spara dina framsteg i molnet och fortsätt spela på vilken enhet som helst",
|
||||
"backups": "Säkerhetskopior",
|
||||
"install_backup": "Installera",
|
||||
"delete_backup": "Ta bort",
|
||||
"create_backup": "Ny säkerhetskopia",
|
||||
"last_backup_date": "Senaste säkerhetskopia {{date}}",
|
||||
"no_backup_preview": "Inga sparfiler hittades för detta spel",
|
||||
"restoring_backup": "Återställer säkerhetskopia ({{progress}} klart)…",
|
||||
"uploading_backup": "Laddar upp säkerhetskopia…",
|
||||
"no_backups": "Du har inte skapat några säkerhetskopior för detta spel än",
|
||||
"backup_uploaded": "Säkerhetskopia uppladdad",
|
||||
"backup_deleted": "Säkerhetskopia borttagen",
|
||||
"backup_restored": "Säkerhetskopia återställd",
|
||||
"see_all_achievements": "Se alla prestationer",
|
||||
"sign_in_to_see_achievements": "Logga in för att se prestationer",
|
||||
"mapping_method_automatic": "Automatisk",
|
||||
"mapping_method_manual": "Manuell",
|
||||
"mapping_method_label": "Kartläggningsmetod",
|
||||
"files_automatically_mapped": "Filer kartlagda automatiskt",
|
||||
"no_backups_created": "Inga säkerhetskopior skapade för detta spel",
|
||||
"manage_files": "Hantera filer",
|
||||
"loading_save_preview": "Söker efter sparfiler…",
|
||||
"wine_prefix": "Wine-prefix",
|
||||
"wine_prefix_description": "Wine-prefixet som används för att köra detta spel",
|
||||
"launch_options": "Startalternativ",
|
||||
"launch_options_description": "Avancerade användare kan lägga till modifieringar till sina startalternativ (experimentell funktion)",
|
||||
"launch_options_placeholder": "Inga parametrar angivna",
|
||||
"no_download_option_info": "Ingen information tillgänglig",
|
||||
"backup_deletion_failed": "Misslyckades med att ta bort säkerhetskopian",
|
||||
"max_number_of_artifacts_reached": "Maximalt antal säkerhetskopior nått för detta spel",
|
||||
"achievements_not_sync": "Se hur du synkroniserar dina prestationer",
|
||||
"manage_files_description": "Hantera vilka filer som ska säkerhetskopieras och återställas",
|
||||
"select_folder": "Välj mapp",
|
||||
"backup_from": "Säkerhetskopia från {{date}}",
|
||||
"automatic_backup_from": "Automatisk säkerhetskopia från {{date}}",
|
||||
"enable_automatic_cloud_sync": "Aktivera automatisk molnsynkronisering",
|
||||
"custom_backup_location_set": "Anpassad plats för säkerhetskopior inställd",
|
||||
"no_directory_selected": "Ingen mapp vald",
|
||||
"no_write_permission": "Kan inte ladda ner till denna mapp. Klicka här för att läsa mer.",
|
||||
"reset_achievements": "Återställ prestationer",
|
||||
"reset_achievements_description": "Detta kommer att återställa alla prestationer för {{game}}",
|
||||
"reset_achievements_title": "Är du säker?",
|
||||
"reset_achievements_success": "Prestationer återställda",
|
||||
"reset_achievements_error": "Misslyckades med att återställa prestationer",
|
||||
"download_error_gofile_quota_exceeded": "Du har överskridit din månadsgräns för Gofile. Vänta tills kvoten återställs.",
|
||||
"download_error_real_debrid_account_not_authorized": "Ditt Real-Debrid-konto är inte auktoriserat att göra nya nedladdningar. Kontrollera dina kontoinställningar och försök igen.",
|
||||
"download_error_not_cached_on_real_debrid": "Denna nedladdning finns inte på Real-Debrid och statusövervakning från Real-Debrid är ännu inte tillgänglig.",
|
||||
"download_error_not_cached_on_torbox": "Denna nedladdning finns inte på TorBox och statusövervakning från TorBox är ännu inte tillgänglig.",
|
||||
"download_error_not_cached_on_hydra": "Denna nedladdning finns inte på Nimbus.",
|
||||
"game_removed_from_favorites": "Spelet togs bort från favoriter",
|
||||
"game_added_to_favorites": "Spelet lades till i favoriter",
|
||||
"automatically_extract_downloaded_files": "Extrahera nedladdade filer automatiskt",
|
||||
"create_start_menu_shortcut": "Skapa genväg i Startmenyn",
|
||||
"invalid_wine_prefix_path": "Ogiltig sökväg för Wine-prefix",
|
||||
"invalid_wine_prefix_path_description": "Sökvägen till Wine-prefixet är ogiltig. Kontrollera sökvägen och försök igen.",
|
||||
"missing_wine_prefix": "Wine-prefix krävs för att skapa en säkerhetskopia på Linux"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Aktivera Hydra",
|
||||
"installation_id": "Installations ID:",
|
||||
"enter_activation_code": "Ange din aktiveringskod",
|
||||
"message": "Om du inte vet var du ska fråga efter denna, borde du inte ha den.",
|
||||
"activate": "Aktivera",
|
||||
"loading": "Laddar…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Fortsätt",
|
||||
"pause": "Pausa",
|
||||
"eta": "Slutförs {{eta}}",
|
||||
"paused": "Pausad",
|
||||
"verifying": "Verifierar…",
|
||||
"completed": "Slutförd",
|
||||
"removed": "Ej nedladdad",
|
||||
"cancel": "Avbryt",
|
||||
"filter": "Filtrera nedladdade spel",
|
||||
"remove": "Ta bort",
|
||||
"downloading_metadata": "Laddar metadata…",
|
||||
"deleting": "Tar bort installationsfil…",
|
||||
"delete": "Ta bort installationsfil",
|
||||
"delete_modal_title": "Är du säker?",
|
||||
"delete_modal_description": "Detta tar bort alla installationsfiler från din dator",
|
||||
"install": "Installera",
|
||||
"download_in_progress": "Pågår",
|
||||
"queued_downloads": "Köade nedladdningar",
|
||||
"downloads_completed": "Klart",
|
||||
"queued": "I kö",
|
||||
"no_downloads_title": "Så tomt",
|
||||
"no_downloads_description": "Du har inte laddat ner något med Hydra än, men det är aldrig för sent att börja.",
|
||||
"checking_files": "Kontrollerar filer…",
|
||||
"seeding": "Seedar",
|
||||
"stop_seeding": "Sluta seeda",
|
||||
"resume_seeding": "Fortsätt seeda",
|
||||
"options": "Hantera",
|
||||
"extract": "Packa upp filer",
|
||||
"extracting": "Packar upp filer…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Nedladdningssökväg",
|
||||
"change": "Uppdatera",
|
||||
"notifications": "Aviseringar",
|
||||
"enable_download_notifications": "När en nedladdning är klar",
|
||||
"enable_repack_list_notifications": "När en ny repack läggs till",
|
||||
"real_debrid_api_token_label": "Real-Debrid API-token",
|
||||
"quit_app_instead_hiding": "Stäng Hydra istället för att minimera",
|
||||
"launch_with_system": "Starta Hydra vid systemstart",
|
||||
"general": "Allmänt",
|
||||
"behavior": "Beteende",
|
||||
"download_sources": "Nedladdningskällor",
|
||||
"language": "Språk",
|
||||
"api_token": "API-token",
|
||||
"enable_real_debrid": "Aktivera Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid är en obegränsad nedladdningstjänst som låter dig ladda ner filer snabbt, endast begränsad av din internetanslutning.",
|
||||
"debrid_invalid_token": "Ogiltig API-token",
|
||||
"debrid_api_token_hint": "Du kan hämta din API-token <0>här</0>",
|
||||
"real_debrid_free_account_error": "Kontot \"{{username}}\" är ett gratiskonto. Prenumerera på Real-Debrid",
|
||||
"debrid_linked_message": "Kontot \"{{username}}\" kopplat",
|
||||
"save_changes": "Spara ändringar",
|
||||
"changes_saved": "Ändringar sparades",
|
||||
"download_sources_description": "Hydra hämtar nedladdningslänkar från dessa källor. Källans URL måste vara en direktlänk till en .json-fil med nedladdningslänkar.",
|
||||
"validate_download_source": "Validera",
|
||||
"remove_download_source": "Ta bort",
|
||||
"add_download_source": "Lägg till källa",
|
||||
"download_count_zero": "Inga nedladdningsalternativ",
|
||||
"download_count_one": "{{countFormatted}} nedladdningsalternativ",
|
||||
"download_count_other": "{{countFormatted}} nedladdningsalternativ",
|
||||
"download_source_url": "URL till nedladdningskälla",
|
||||
"add_download_source_description": "Ange URL:en till .json-filen",
|
||||
"download_source_up_to_date": "Uppdaterad",
|
||||
"download_source_errored": "Fel uppstod",
|
||||
"sync_download_sources": "Synkronisera källor",
|
||||
"removed_download_source": "Nedladdningskälla borttagen",
|
||||
"removed_download_sources": "Nedladdningskällor borttagna",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nej",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, ta bort allt",
|
||||
"title_confirmation_delete_all_sources": "Ta bort alla nedladdningskällor",
|
||||
"description_confirmation_delete_all_sources": "Du kommer att ta bort alla nedladdningskällor",
|
||||
"button_delete_all_sources": "Ta bort alla",
|
||||
"added_download_source": "Nedladdningskälla tillagd",
|
||||
"download_sources_synced": "Alla nedladdningskällor är synkroniserade",
|
||||
"insert_valid_json_url": "Ange en giltig JSON-URL",
|
||||
"found_download_option_zero": "Inga nedladdningsalternativ hittades",
|
||||
"found_download_option_one": "Hittade {{countFormatted}} nedladdningsalternativ",
|
||||
"found_download_option_other": "Hittade {{countFormatted}} nedladdningsalternativ",
|
||||
"import": "Importera",
|
||||
"public": "Offentlig",
|
||||
"private": "Privat",
|
||||
"friends_only": "Endast vänner",
|
||||
"privacy": "Integritet",
|
||||
"profile_visibility": "Profilens synlighet",
|
||||
"profile_visibility_description": "Välj vem som kan se din profil och ditt bibliotek",
|
||||
"required_field": "Detta fält är obligatoriskt",
|
||||
"source_already_exists": "Denna källa har redan lagts till",
|
||||
"must_be_valid_url": "Källan måste vara en giltig URL",
|
||||
"blocked_users": "Blockerade användare",
|
||||
"user_unblocked": "Användaren har avblockerats",
|
||||
"enable_achievement_notifications": "När en prestation låses upp",
|
||||
"launch_minimized": "Starta Hydra minimerad",
|
||||
"disable_nsfw_alert": "Inaktivera NSFW-varning",
|
||||
"seed_after_download_complete": "Seeda efter att nedladdningen är klar",
|
||||
"show_hidden_achievement_description": "Visa beskrivning av dolda prestationer innan de låses upp",
|
||||
"account": "Konto",
|
||||
"no_users_blocked": "Du har inga blockerade användare",
|
||||
"subscription_active_until": "Ditt Hydra Cloud är aktivt till {{date}}",
|
||||
"manage_subscription": "Hantera prenumeration",
|
||||
"update_email": "Uppdatera e-postadress",
|
||||
"update_password": "Uppdatera lösenord",
|
||||
"current_email": "Nuvarande e-postadress:",
|
||||
"no_email_account": "Du har ännu inte angett någon e-postadress",
|
||||
"account_data_updated_successfully": "Kontoinformationen har uppdaterats",
|
||||
"renew_subscription": "Förnya Hydra Cloud",
|
||||
"subscription_expired_at": "Din prenumeration gick ut den {{date}}",
|
||||
"no_subscription": "Njut av Hydra på bästa möjliga sätt",
|
||||
"become_subscriber": "Bli Hydra Cloud-prenumerant",
|
||||
"subscription_renew_cancelled": "Automatisk förnyelse är inaktiverad",
|
||||
"subscription_renews_on": "Din prenumeration förnyas den {{date}}",
|
||||
"bill_sent_until": "Din nästa faktura skickas senast detta datum",
|
||||
"no_themes": "Det verkar som att du inte har några teman ännu, men ingen fara – klicka här för att skapa ditt första mästerverk.",
|
||||
"editor_tab_code": "Kod",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Spara",
|
||||
"web_store": "Webbutik",
|
||||
"clear_themes": "Rensa",
|
||||
"create_theme": "Skapa",
|
||||
"create_theme_modal_title": "Skapa eget tema",
|
||||
"create_theme_modal_description": "Skapa ett nytt tema för att anpassa Hydras utseende",
|
||||
"theme_name": "Namn",
|
||||
"insert_theme_name": "Ange temats namn",
|
||||
"set_theme": "Aktivera tema",
|
||||
"unset_theme": "Avaktivera tema",
|
||||
"delete_theme": "Ta bort tema",
|
||||
"edit_theme": "Redigera tema",
|
||||
"delete_all_themes": "Ta bort alla teman",
|
||||
"delete_all_themes_description": "Detta kommer att ta bort alla dina egna teman",
|
||||
"delete_theme_description": "Detta kommer att ta bort temat {{theme}}",
|
||||
"cancel": "Avbryt",
|
||||
"appearance": "Utseende",
|
||||
"enable_torbox": "Aktivera TorBox",
|
||||
"torbox_description": "TorBox är din premium seedbox-tjänst som konkurrerar med de bästa servrarna på marknaden.",
|
||||
"torbox_account_linked": "TorBox-konto kopplat",
|
||||
"create_real_debrid_account": "Klicka här om du ännu inte har ett Real-Debrid-konto",
|
||||
"create_torbox_account": "Klicka här om du ännu inte har ett TorBox-konto",
|
||||
"real_debrid_account_linked": "Real-Debrid-konto kopplat",
|
||||
"name_min_length": "Temanamnet måste innehålla minst 3 tecken",
|
||||
"import_theme": "Importera tema",
|
||||
"import_theme_description": "Du kommer att importera {{theme}} från temabutiken",
|
||||
"error_importing_theme": "Fel vid import av tema",
|
||||
"theme_imported": "Temat har importerats",
|
||||
"enable_friend_request_notifications": "När en vänförfrågan tas emot",
|
||||
"enable_auto_install": "Ladda ner uppdateringar automatiskt",
|
||||
"common_redist": "Nödvändiga systemkomponenter",
|
||||
"common_redist_description": "Nödvändiga systemkomponenter krävs för att vissa spel ska fungera. Det rekommenderas att installera dem för att undvika problem.",
|
||||
"install_common_redist": "Installera",
|
||||
"installing_common_redist": "Installerar…",
|
||||
"show_download_speed_in_megabytes": "Visa nedladdningshastighet i megabyte per sekund",
|
||||
"extract_files_by_default": "Extrahera filer automatiskt efter nedladdning",
|
||||
"achievement_custom_notification_position": "Anpassad position för prestationmeddelande",
|
||||
"top-left": "Övre vänster",
|
||||
"top-center": "Övre mitten",
|
||||
"top-right": "Övre höger",
|
||||
"bottom-left": "Nedre vänster",
|
||||
"bottom-center": "Nedre mitten",
|
||||
"bottom-right": "Nedre höger",
|
||||
"enable_achievement_custom_notifications": "Aktivera anpassade prestationmeddelanden",
|
||||
"alignment": "Justering",
|
||||
"variation": "Variation",
|
||||
"default": "Standard",
|
||||
"rare": "Sällsynt",
|
||||
"platinum": "Platina",
|
||||
"hidden": "Dold",
|
||||
"test_notification": "Testa meddelande",
|
||||
"notification_preview": "Förhandsvisning av prestationmeddelande",
|
||||
"enable_friend_start_game_notifications": "När en vän börjar spela ett spel"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Nedladdning klar",
|
||||
"game_ready_to_install": "{{title}} är redo att installeras",
|
||||
"repack_list_updated": "Repack-listan har uppdaterats",
|
||||
"repack_count_one": "{{count}} repack tillagd",
|
||||
"repack_count_other": "{{count}} repacks tillagda",
|
||||
"new_update_available": "Version {{version}} tillgänglig",
|
||||
"restart_to_install_update": "Starta om Hydra för att installera uppdateringen",
|
||||
"notification_achievement_unlocked_title": "Prestation upplåst för {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} och {{count}} andra har låsts upp",
|
||||
"new_friend_request_description": "{{displayName}} har skickat en vänförfrågan",
|
||||
"new_friend_request_title": "Ny vänförfrågan",
|
||||
"extraction_complete": "Extrahering slutförd",
|
||||
"game_extracted": "{{title}} har extraherats",
|
||||
"friend_started_playing_game": "{{displayName}} började spela ett spel",
|
||||
"test_achievement_notification_title": "Detta är ett testmeddelande",
|
||||
"test_achievement_notification_description": "Ganska coolt, eller hur?"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Öppna Hydra",
|
||||
"quit": "Avsluta"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Tillgänglig",
|
||||
"available_other": "Tillgänglig",
|
||||
"no_downloads": "Inga nedladdningar tillgängliga"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Program inte installerade",
|
||||
"description": "Wine- eller Lutris-körbara filer hittades inte på ditt system",
|
||||
"instructions": "Kontrollera hur du installerar dem korrekt på din Linux-distribution så att spelet kan köras normalt"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Stäng-knapp"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Visa/dölj lösenord"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} timmar",
|
||||
"amount_minutes": "{{amount}} minuter",
|
||||
"last_time_played": "Senast spelad {{period}}",
|
||||
"activity": "Senaste aktivitet",
|
||||
"library": "Bibliotek",
|
||||
"total_play_time": "Total speltid",
|
||||
"no_recent_activity_title": "Hmmm… ingenting här",
|
||||
"no_recent_activity_description": "Du har inte spelat några spel nyligen. Dags att ändra på det!",
|
||||
"display_name": "Visningsnamn",
|
||||
"saving": "Sparar",
|
||||
"save": "Spara",
|
||||
"edit_profile": "Redigera profil",
|
||||
"saved_successfully": "Sparat",
|
||||
"try_again": "Försök igen",
|
||||
"sign_out_modal_title": "Är du säker?",
|
||||
"cancel": "Avbryt",
|
||||
"successfully_signed_out": "Utloggningen lyckades",
|
||||
"sign_out": "Logga ut",
|
||||
"playing_for": "Spelar sedan {{amount}}",
|
||||
"sign_out_modal_text": "Ditt bibliotek är kopplat till det aktuella kontot. När du loggar ut kommer biblioteket inte längre vara synligt, och framstegen kommer inte att sparas. Vill du fortsätta logga ut?",
|
||||
"add_friends": "Lägg till vänner",
|
||||
"add": "Lägg till",
|
||||
"friend_code": "Vänkod",
|
||||
"see_profile": "Visa profil",
|
||||
"sending": "Skickar",
|
||||
"friend_request_sent": "Vänförfrågan skickad",
|
||||
"friends": "Vänner",
|
||||
"friends_list": "Vänlista",
|
||||
"user_not_found": "Användare hittades inte",
|
||||
"block_user": "Blockera användare",
|
||||
"add_friend": "Lägg till vän",
|
||||
"request_sent": "Förfrågan skickad",
|
||||
"request_received": "Förfrågan mottagen",
|
||||
"accept_request": "Acceptera förfrågan",
|
||||
"ignore_request": "Ignorera förfrågan",
|
||||
"cancel_request": "Avbryt förfrågan",
|
||||
"undo_friendship": "Ta bort vänskap",
|
||||
"request_accepted": "Förfrågan accepterad",
|
||||
"user_blocked_successfully": "Användaren har blockerats",
|
||||
"user_block_modal_text": "Detta kommer att blockera {{displayName}}",
|
||||
"blocked_users": "Blockerade användare",
|
||||
"unblock": "Avblockera",
|
||||
"no_friends_added": "Du har inte lagt till några vänner",
|
||||
"pending": "Väntande",
|
||||
"no_pending_invites": "Du har inga väntande inbjudningar",
|
||||
"no_blocked_users": "Du har inga blockerade användare",
|
||||
"friend_code_copied": "Vänkod kopierad",
|
||||
"undo_friendship_modal_text": "Detta kommer att ta bort din vänskap med {{displayName}}",
|
||||
"privacy_hint": "För att justera vem som kan se detta, gå till <0>Inställningar</0>",
|
||||
"locked_profile": "Denna profil är privat",
|
||||
"image_process_failure": "Fel vid bildbehandling",
|
||||
"required_field": "Detta fält är obligatoriskt",
|
||||
"displayname_min_length": "Visningsnamnet måste vara minst 3 tecken långt",
|
||||
"displayname_max_length": "Visningsnamnet får vara högst 50 tecken långt",
|
||||
"report_profile": "Anmäl denna profil",
|
||||
"report_reason": "Varför anmäler du denna profil?",
|
||||
"report_description": "Ytterligare information",
|
||||
"report_description_placeholder": "Ytterligare information",
|
||||
"report": "Anmäl",
|
||||
"report_reason_hate": "Hatretorik",
|
||||
"report_reason_sexual_content": "Sexuellt innehåll",
|
||||
"report_reason_violence": "Våld",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Annat",
|
||||
"profile_reported": "Profil anmäld",
|
||||
"your_friend_code": "Din vänkod:",
|
||||
"upload_banner": "Ladda upp banner",
|
||||
"uploading_banner": "Laddar upp banner…",
|
||||
"background_image_updated": "Bakgrundsbild uppdaterad",
|
||||
"stats": "Statistik",
|
||||
"achievements": "prestationer",
|
||||
"games": "Spel",
|
||||
"top_percentile": "Topp {{percentile}}%",
|
||||
"ranking_updated_weekly": "Rankingen uppdateras varje vecka",
|
||||
"playing": "Spelar {{game}}",
|
||||
"achievements_unlocked": "Prestationer upplåsta",
|
||||
"earned_points": "Intjänade poäng",
|
||||
"show_achievements_on_profile": "Visa dina prestationer på profilen",
|
||||
"show_points_on_profile": "Visa dina intjänade poäng på din profil"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Prestationer upplåst",
|
||||
"user_achievements": "Prestationer för {{displayName}}",
|
||||
"your_achievements": "Dina prestationer",
|
||||
"unlocked_at": "Upplåst den: {{date}}",
|
||||
"subscription_needed": "Ett Hydra Cloud-abonnemang krävs för att se detta innehåll",
|
||||
"new_achievements_unlocked": "Upplåste {{achievementCount}} nya prestationer från {{gameCount}} spel",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} prestationer",
|
||||
"achievements_unlocked_for_game": "Upplåste {{achievementCount}} nya prestationer för {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Detta är en dold prestation",
|
||||
"achievement_earn_points": "Tjäna {{points}} poäng med denna prestation",
|
||||
"earned_points": "Tjänade poäng:",
|
||||
"available_points": "Tillgängliga poäng:",
|
||||
"how_to_earn_achievements_points": "Hur tjänar man poäng på prestationer?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud-abonnemang",
|
||||
"subscribe_now": "Prenumerera nu",
|
||||
"cloud_saving": "Spara i molnet",
|
||||
"cloud_achievements": "Spara dina prestationer i molnet",
|
||||
"animated_profile_picture": "Animerade profilbilder",
|
||||
"premium_support": "Premium-support",
|
||||
"show_and_compare_achievements": "Visa och jämför dina prestationer med andra användare",
|
||||
"animated_profile_banner": "Animerad profilbanner",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Du har just upptäckt en Hydra Cloud-funktion!",
|
||||
"learn_more": "Läs mer",
|
||||
"debrid_description": "Ladda ner upp till 4x snabbare med Nimbus"
|
||||
}
|
||||
}
|
||||
@@ -8,46 +8,46 @@
|
||||
"surprise_me": "Beni Şaşırt",
|
||||
"no_results": "Sonuç bulunamadı",
|
||||
"start_typing": "Aramak için yazmaya başlayın...",
|
||||
"hot": "Şu anda popüler",
|
||||
"weekly": "📅 Haftanın en iyi oyunları",
|
||||
"achievements": "🏆 Tamamlanacak oyunlar"
|
||||
"hot": "Şu anda Popüler",
|
||||
"weekly": "📅 Haftanın En İyi Oyunları",
|
||||
"achievements": "🏆 Bitirilecek Oyunlar"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirilenler",
|
||||
"settings": "Ayarlar",
|
||||
"my_library": "Kütüphane",
|
||||
"my_library": "Kütüphanem",
|
||||
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
|
||||
"paused": "{{title}} (Durduruldu)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphaneyi filtrele",
|
||||
"paused": "{{title}} (Duraklatıldı)",
|
||||
"downloading": "{{title}} (%{{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphanede filtrele",
|
||||
"home": "Ana Sayfa",
|
||||
"queued": "{{title}} (Sırada)",
|
||||
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
||||
"sign_in": "Giriş yap",
|
||||
"game_has_no_executable": "Bu oyun için çalıştırılabilir dosya seçilmedi",
|
||||
"sign_in": "Giriş Yap",
|
||||
"friends": "Arkadaşlar",
|
||||
"need_help": "Yardıma mı ihtiyacınız var?",
|
||||
"favorites": "Favoriler"
|
||||
},
|
||||
"header": {
|
||||
"search": "Oyunları ara",
|
||||
"search": "Oyunlarda Ara",
|
||||
"home": "Ana Sayfa",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirilenler",
|
||||
"search_results": "Arama sonuçları",
|
||||
"search_results": "Arama Sonuçları",
|
||||
"settings": "Ayarlar",
|
||||
"version_available_install": "{{version}} sürümü mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "{{version}} sürümü mevcut. İndirmek için buraya tıklayın."
|
||||
"version_available_install": "{{version}} sürümü mevcut. Yeniden başlatıp yüklemek için tıklayın.",
|
||||
"version_available_download": "{{version}} sürümü mevcut. İndirmek için tıklayın."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Devam eden indirme yok",
|
||||
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlanma: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
|
||||
"downloading": "{{title}} indiriliyor… (%{{percentage}} tamamlandı) - Bitiş: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… (%{{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… (%{{percentage}} tamamlandı)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "İndirme tamamlandı",
|
||||
"installation_complete_message": "Genel bağımlılıklar başarıyla yüklendi."
|
||||
"installation_complete": "Kurulum tamamlandı",
|
||||
"installation_complete_message": "Gerekli paketler başarıyla yüklendi"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filtrele…",
|
||||
@@ -58,7 +58,7 @@
|
||||
"download_sources": "İndirme kaynakları",
|
||||
"result_count": "{{resultCount}} sonuç",
|
||||
"filter_count": "{{filterCount}} mevcut",
|
||||
"clear_filters": "{{filterCount}} seçili filtreyi temizle"
|
||||
"clear_filters": "{{filterCount}} seçiliyi temizle"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "İndirme seçeneklerini aç",
|
||||
@@ -67,32 +67,32 @@
|
||||
"download_options_other": "{{count}} indirme seçeneği",
|
||||
"updated_at": "{{updated_at}} tarihinde güncellendi",
|
||||
"install": "Yükle",
|
||||
"resume": "Devam et",
|
||||
"pause": "Durdur",
|
||||
"cancel": "İptal et",
|
||||
"resume": "Devam Et",
|
||||
"pause": "Duraklat",
|
||||
"cancel": "İptal Et",
|
||||
"remove": "Kaldır",
|
||||
"space_left_on_disk": "Diskte {{space}} boş alan kaldı",
|
||||
"eta": "{{eta}} tahmini bitiş",
|
||||
"eta": "Bitiş: {{eta}}",
|
||||
"calculating_eta": "Kalan süre hesaplanıyor…",
|
||||
"downloading_metadata": "Meta veriler indiriliyor…",
|
||||
"filter": "Paketleri filtrele",
|
||||
"requirements": "Sistem gereksinimleri",
|
||||
"requirements": "Sistem Gereksinimleri",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Önerilen",
|
||||
"paused": "Durduruldu",
|
||||
"paused": "Duraklatıldı",
|
||||
"release_date": "{{date}} tarihinde yayımlandı",
|
||||
"publisher": "{{publisher}} tarafından yayımlandı",
|
||||
"hours": "saat",
|
||||
"minutes": "dakika",
|
||||
"amount_hours": "{{amount}} saat",
|
||||
"amount_minutes": "{{amount}} dakika",
|
||||
"accuracy": "{{accuracy}}% doğruluk",
|
||||
"accuracy": "%{{accuracy}} doğruluk",
|
||||
"add_to_library": "Kütüphaneye ekle",
|
||||
"remove_from_library": "Kütüphaneden kaldır",
|
||||
"no_downloads": "İndirilebilir içerik yok",
|
||||
"play_time": "{{amount}} süre oynandı",
|
||||
"last_time_played": "Son oynama {{period}} önce",
|
||||
"not_played_yet": "{{title}} henüz oynanmadı",
|
||||
"no_downloads": "İndirme mevcut değil",
|
||||
"play_time": "{{amount}} oynandı",
|
||||
"last_time_played": "Son oynanma: {{period}}",
|
||||
"not_played_yet": "{{title}} oyununu henüz oynamadınız",
|
||||
"next_suggestion": "Sonraki öneri",
|
||||
"play": "Oyna",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
@@ -107,134 +107,140 @@
|
||||
"download_path": "İndirme yolu",
|
||||
"previous_screenshot": "Önceki ekran görüntüsü",
|
||||
"next_screenshot": "Sonraki ekran görüntüsü",
|
||||
"screenshot": "{{number}} ekran görüntüsü",
|
||||
"open_screenshot": "{{number}} ekran görüntüsünü aç",
|
||||
"screenshot": "Ekran görüntüsü {{number}}",
|
||||
"open_screenshot": "Ekran görüntüsünü aç ({{number}})",
|
||||
"download_settings": "İndirme ayarları",
|
||||
"downloader": "İndirici",
|
||||
"select_executable": "Seç",
|
||||
"no_executable_selected": "Hiçbir çalıştırılabilir dosya seçilmedi",
|
||||
"no_executable_selected": "Çalıştırılabilir dosya seçilmedi",
|
||||
"open_folder": "Klasörü aç",
|
||||
"open_download_location": "İndirilen dosyaları gör",
|
||||
"open_download_location": "İndirilen dosyaları görüntüle",
|
||||
"create_shortcut": "Masaüstü kısayolu oluştur",
|
||||
"clear": "Temizle",
|
||||
"remove_files": "Dosyaları kaldır",
|
||||
"remove_from_library_title": "Emin misiniz?",
|
||||
"remove_from_library_description": "Bu işlem sonrasında {{game}} oyunu kütüphanenizden kaldıracaktır",
|
||||
"remove_from_library_description": "{{game}} oyununu kütüphanenizden kaldıracaktır",
|
||||
"options": "Seçenekler",
|
||||
"executable_section_title": "Çalıştırılabilir dosya",
|
||||
"executable_section_description": "\"Oyna\" butonuna tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"downloads_section_title": "İndirmeler",
|
||||
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
|
||||
"danger_zone_section_title": "Tehlike bölgesi",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden kaldırın veya Hydra tarafından indirilen dosyaları silin.",
|
||||
"download_in_progress": "İndirme devam ediyor",
|
||||
"download_paused": "İndirme durduruldu",
|
||||
"executable_section_description": "\"Oyna\" seçildiğinde çalışacak dosyanın yolu",
|
||||
"downloads_section_title": "İndirilenler",
|
||||
"downloads_section_description": "Bu oyunun güncelleme veya diğer sürümlerine göz atın",
|
||||
"danger_zone_section_title": "Tehlikeli Alan",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyalardan kaldırın",
|
||||
"download_in_progress": "İndirme sürüyor",
|
||||
"download_paused": "İndirme duraklatıldı",
|
||||
"last_downloaded_option": "Son indirilen seçenek",
|
||||
"create_steam_shortcut": "Steam kısayolu oluştur",
|
||||
"create_shortcut_success": "Kısayol başarıyla oluşturuldu",
|
||||
"you_might_need_to_restart_steam": "Değişiklikleri görmek için Steam'i yeniden başlatmanız gerekebilir",
|
||||
"create_shortcut_error": "Kısayol oluşturulurken hata oluştu",
|
||||
"nsfw_content_title": "Bu oyun uygunsuz içerik içeriyor",
|
||||
"nsfw_content_description": "{{title}} her yaş için uygun olmayabilecek içeriklere sahiptir. Devam etmek istediğinizden emin misiniz?",
|
||||
"nsfw_content_title": "Bu oyun uygunsuz içerik barındırıyor",
|
||||
"nsfw_content_description": "{{title}} bazı kullanıcılar için uygun olmayabilecek içerik barındırıyor. Devam etmek istediğinizden emin misiniz?",
|
||||
"allow_nsfw_content": "Devam et",
|
||||
"refuse_nsfw_content": "Geri dön",
|
||||
"stats": "İstatistikler",
|
||||
"download_count": "İndirme sayısı",
|
||||
"download_count": "İndirme",
|
||||
"player_count": "Aktif oyuncular",
|
||||
"download_error": "Bu indirme seçeneği mevcut değil",
|
||||
"download_error": "Bu indirme seçeneği kullanılamıyor",
|
||||
"download": "İndir",
|
||||
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
|
||||
"executable_path_in_use": "Çalıştırılabilir dosya zaten \"{{game}}\" tarafından kullanılıyor",
|
||||
"warning": "Uyarı:",
|
||||
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
|
||||
"hydra_needs_to_remain_open": "Bu indirme için, Hydra programının tamamlanana kadar açık kalması gerekir. Hydra kapanırsa, ilerlemeniz kaybolacaktır.",
|
||||
"achievements": "Başarımlar",
|
||||
"achievements_count": "Başarımlar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Bulut kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
|
||||
"cloud_save": "Bulut Kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazdan devam edin",
|
||||
"backups": "Yedekler",
|
||||
"install_backup": "Yükle",
|
||||
"delete_backup": "Sil",
|
||||
"create_backup": "Yeni yedek oluştur",
|
||||
"last_backup_date": "{{date}} tarihindeki son yedek",
|
||||
"no_backup_preview": "Bu oyun için bir kayıt dosyası bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
|
||||
"create_backup": "Yeni Yedek",
|
||||
"last_backup_date": "Son yedekleme: {{date}}",
|
||||
"no_backup_preview": "Bu başlık için kayıtlı oyun bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor (%{{progress}} tamamlandı)…",
|
||||
"uploading_backup": "Yedek yükleniyor…",
|
||||
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
|
||||
"no_backups": "Bu oyun için henüz yedek oluşturmadınız",
|
||||
"backup_uploaded": "Yedek yüklendi",
|
||||
"backup_deleted": "Yedek silindi",
|
||||
"backup_restored": "Yedek geri yüklendi",
|
||||
"see_all_achievements": "Tüm başarımları gör",
|
||||
"sign_in_to_see_achievements": "Başarımları görmek için oturum açın",
|
||||
"see_all_achievements": "Tüm başarımları görüntüle",
|
||||
"sign_in_to_see_achievements": "Başarımları görmek için giriş yapın",
|
||||
"mapping_method_automatic": "Otomatik",
|
||||
"mapping_method_manual": "Manuel",
|
||||
"mapping_method_label": "Eşleme yöntemi",
|
||||
"files_automatically_mapped": "Dosyalar otomatik olarak eşlendi",
|
||||
"no_backups_created": "Bu oyun için yedek oluşturulmadı",
|
||||
"mapping_method_label": "Eşleme metodu",
|
||||
"files_automatically_mapped": "Dosyalar otomatik eşlendi",
|
||||
"no_backups_created": "Bu oyun için oluşturulmuş yedek yok",
|
||||
"manage_files": "Dosyaları yönet",
|
||||
"loading_save_preview": "Kayıtlı oyunlar aranıyor…",
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
|
||||
"wine_prefix": "Wine Ön Ek",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine ön eki",
|
||||
"launch_options": "Başlatma Seçenekleri",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine parametreler girebilir (deneysel özellik)",
|
||||
"launch_options_placeholder": "Belirtilen bir parametre yok",
|
||||
"launch_options_description": "Gelişmiş kullanıcılar için başlatma parametreleri tanımlayın (deneysel özellik)",
|
||||
"launch_options_placeholder": "Parametre belirtilmedi",
|
||||
"no_download_option_info": "Bilgi mevcut değil",
|
||||
"backup_deletion_failed": "Yedek silinemedi",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarımlarınızı senkronize etmeyi öğrenin",
|
||||
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
|
||||
"backup_deletion_failed": "Yedek silme işlemi başarısız oldu",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için azami yedekleme sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarımlarını eşitlemeyi öğren",
|
||||
"manage_files_description": "Hangi dosyaların yedekleneceğini ve geri yükleneceğini yönetin",
|
||||
"select_folder": "Klasör seç",
|
||||
"backup_from": "{{date}} tarihinden yedek",
|
||||
"automatic_backup_from": "{{date}} tarihinden otomatik kayıt",
|
||||
"enable_automatic_cloud_sync": "Otomatik bulut kaydı senkronizasyonunu aktifleştir",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
|
||||
"no_directory_selected": "Bir dizin seçilmedi",
|
||||
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
|
||||
"backup_from": "{{date}} tarihli yedek",
|
||||
"automatic_backup_from": "{{date}} tarihli otomatik yedek",
|
||||
"enable_automatic_cloud_sync": "Otomatik bulut eşitlemesini etkinleştir",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu belirlendi",
|
||||
"no_directory_selected": "Klasör seçilmedi",
|
||||
"no_write_permission": "Bu klasöre indirme yapılamıyor. Detaylar için buraya tıklayın.",
|
||||
"reset_achievements": "Başarımları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarımları sıfırlar",
|
||||
"reset_achievements_description": "{{game}} için tüm başarımlar sıfırlanacak",
|
||||
"reset_achievements_title": "Emin misiniz?",
|
||||
"reset_achievements_success": "Başarımlar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarımlar sıfırlanamadı",
|
||||
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı doldurdunuz. Kotanın yenilenmesini bekleyin.",
|
||||
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirme işlemleri yapmak için yetkilendirilmemiş. Lütfen hesap ayarlarınızı kontrol edip tekrar deneyin.",
|
||||
"download_error_not_cached_on_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve Real-Debrid'den indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_on_torbox": "Bu indirme TorBox'ta mevcut değil ve TorBox'tan indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_on_hydra": "Bu indirme Nimbus'ta mevcut değil.",
|
||||
"game_removed_from_favorites": "Oyun favorilerden silindi",
|
||||
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı aştınız. Lütfen kotanın sıfırlanmasını bekleyin.",
|
||||
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirmeler için yetkili değil. Hesap ayarlarınızı kontrol edip tekrar deneyin.",
|
||||
"download_error_not_cached_on_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve durum sorgulanamıyor.",
|
||||
"download_error_not_cached_on_torbox": "Bu indirme TorBox üzerinde mevcut değil ve durum sorgulanamıyor.",
|
||||
"download_error_not_cached_on_hydra": "Bu indirme Nimbus üzerinde mevcut değil.",
|
||||
"game_removed_from_favorites": "Oyun favorilerden kaldırıldı",
|
||||
"game_added_to_favorites": "Oyun favorilere eklendi",
|
||||
"automatically_extract_downloaded_files": "Yüklenmiş dosyaları otomatik olarak çıkart"
|
||||
"automatically_extract_downloaded_files": "İndirilen dosyaları otomatik çıkart",
|
||||
"create_start_menu_shortcut": "Başlat Menüsüne kısayol oluştur",
|
||||
"invalid_wine_prefix_path": "Geçersiz Wine ön ek yolu",
|
||||
"invalid_wine_prefix_path_description": "Wine ön ek yolu hatalı. Lütfen yolu kontrol edin ve tekrar deneyin.",
|
||||
"missing_wine_prefix": "Linux'ta yedekleme oluşturmak için Wine ön eki gereklidir"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'yı Aktive Et",
|
||||
"installation_id": "Kurulum Kimliği:",
|
||||
"enter_activation_code": "Aktivasyon kodunuzu girin",
|
||||
"message": "Bunu nasıl edineceğini bilmiyorsan, buna sahip olmamalısın.",
|
||||
"activate": "Aktive Et",
|
||||
"title": "Hydra'yı Etkinleştir",
|
||||
"installation_id": "Kurulum ID:",
|
||||
"enter_activation_code": "Etkinleştirme kodunu girin",
|
||||
"message": "Bu kodun nereden alınacağını bilmiyorsanız, zaten bu kodu kullanmamanız gerekir.",
|
||||
"activate": "Etkinleştir",
|
||||
"loading": "Yükleniyor…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Devam Et",
|
||||
"pause": "Duraklat",
|
||||
"eta": "Tamamlama {{eta}}",
|
||||
"eta": "Bitiş: {{eta}}",
|
||||
"paused": "Duraklatıldı",
|
||||
"verifying": "Doğrulanıyor…",
|
||||
"completed": "Tamamlandı",
|
||||
"removed": "İndirilmedi",
|
||||
"cancel": "İptal Et",
|
||||
"filter": "İndirilen oyunları filtrele",
|
||||
"filter": "İndirilen oyunlarda filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Meta verileri indiriliyor…",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"delete": "Yükleyiciyi kaldır",
|
||||
"delete_modal_title": "Emin misiniz?",
|
||||
"delete_modal_description": "Bu işlem, tüm kurulum dosyalarını bilgisayarınızdan kaldıracaktır",
|
||||
"install": "Kur",
|
||||
"delete_modal_description": "Tüm kurulum dosyaları bilgisayarınızdan kaldırılacaktır",
|
||||
"install": "Yükle",
|
||||
"download_in_progress": "Devam ediyor",
|
||||
"queued_downloads": "Sıradaki indirmeler",
|
||||
"downloads_completed": "Tamamlananlar",
|
||||
"queued": "Sırada",
|
||||
"no_downloads_title": "Bomboş",
|
||||
"no_downloads_description": "Henüz Hydra ile 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 +249,202 @@
|
||||
"downloads_path": "İndirme yolu",
|
||||
"change": "Güncelle",
|
||||
"notifications": "Bildirimler",
|
||||
"enable_download_notifications": "Bir indirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
|
||||
"enable_download_notifications": "İndirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir paket eklendiğinde",
|
||||
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatınca sistem tepsisine gitmesin",
|
||||
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
|
||||
"quit_app_instead_hiding": "Hydra kapatıldığında gizleme",
|
||||
"launch_with_system": "Sistem başlatıldığında Hydra'yı 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 +455,79 @@
|
||||
"request_sent": "İstek gönderildi",
|
||||
"request_received": "İstek alındı",
|
||||
"accept_request": "İsteği kabul et",
|
||||
"ignore_request": "İsteği yok say",
|
||||
"ignore_request": "İsteği görmezden gel",
|
||||
"cancel_request": "İsteği iptal et",
|
||||
"undo_friendship": "Arkadaşlığı sonlandır",
|
||||
"undo_friendship": "Arkadaşlığı kaldır",
|
||||
"request_accepted": "İstek kabul edildi",
|
||||
"user_blocked_successfully": "Kullanıcı başarıyla engellendi",
|
||||
"user_block_modal_text": "Bu işlem {{displayName}} adlı kullanıcıyı engelleyecek",
|
||||
"user_block_modal_text": "{{displayName}} engellenecek",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"unblock": "Engeli kaldır",
|
||||
"no_friends_added": "Hiç arkadaş eklemediniz",
|
||||
"pending": "Bekliyor",
|
||||
"no_friends_added": "Hiç arkadaşınız yok",
|
||||
"pending": "Bekleyen",
|
||||
"no_pending_invites": "Bekleyen davetiniz yok",
|
||||
"no_blocked_users": "Engellenmiş kullanıcı yok",
|
||||
"no_blocked_users": "Engellenen kullanıcı yok",
|
||||
"friend_code_copied": "Arkadaş kodu kopyalandı",
|
||||
"undo_friendship_modal_text": "Bu işlem {{displayName}} ile arkadaşlığınızı sonlandıracak",
|
||||
"privacy_hint": "Bunu kimin görebileceğini ayarlamak için <0>Ayarlar</0> bölümüne gidin",
|
||||
"undo_friendship_modal_text": "Bu işlemle {{displayName}} ile arkadaşlığınız kaldırılacak",
|
||||
"privacy_hint": "Bunu kimlerin görebileceğini <0>Ayarlar</0> bölümünden değiştirebilirsiniz",
|
||||
"locked_profile": "Bu profil gizli",
|
||||
"image_process_failure": "Görüntü işleme başarısız oldu",
|
||||
"required_field": "Bu alan gerekli",
|
||||
"displayname_min_length": "Görünen isim en az 3 karakter uzunluğunda olmalıdır",
|
||||
"displayname_max_length": "Görünen isim en fazla 50 karakter uzunluğunda olabilir",
|
||||
"report_profile": "Bu profili bildir",
|
||||
"report_reason": "Bu profili neden bildiriyorsunuz?",
|
||||
"image_process_failure": "Resim işlenirken hata oluştu",
|
||||
"required_field": "Bu alan gereklidir",
|
||||
"displayname_min_length": "Kullanıcı adı en az 3 karakter olmalıdır",
|
||||
"displayname_max_length": "Kullanıcı adı en fazla 50 karakter olmalıdır",
|
||||
"report_profile": "Bu profili şikayet et",
|
||||
"report_reason": "Bu profili neden şikayet ediyorsunuz?",
|
||||
"report_description": "Ek bilgi",
|
||||
"report_description_placeholder": "Ek bilgi",
|
||||
"report": "Bildir",
|
||||
"report": "Şikayet et",
|
||||
"report_reason_hate": "Nefret söylemi",
|
||||
"report_reason_sexual_content": "Cinsel içerik",
|
||||
"report_reason_violence": "Şiddet",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Diğer",
|
||||
"profile_reported": "Profil bildirildi",
|
||||
"profile_reported": "Profil şikayet edildi",
|
||||
"your_friend_code": "Arkadaş kodunuz:",
|
||||
"upload_banner": "Afiş yükle",
|
||||
"uploading_banner": "Afiş yükleniyor…",
|
||||
"background_image_updated": "Arka plan görüntüsü güncellendi",
|
||||
"upload_banner": "Banner yükle",
|
||||
"uploading_banner": "Banner yükleniyor…",
|
||||
"background_image_updated": "Arka plan resmi güncellendi",
|
||||
"stats": "İstatistikler",
|
||||
"achievements": "Başarımlar",
|
||||
"games": "Oyunlar",
|
||||
"top_percentile": "En üst {{percentile}}%",
|
||||
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
|
||||
"top_percentile": "En iyi %{{percentile}}",
|
||||
"ranking_updated_weekly": "Sıralama haftalık güncellenir",
|
||||
"playing": "{{game}} oynanıyor",
|
||||
"achievements_unlocked": "Başarımlar 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"sign_in": "Увійти",
|
||||
"favorites": "Улюблені",
|
||||
"friends": "Друзі",
|
||||
"need_help": "Потрібна допомога?"
|
||||
"need_help": "Потрібна допомога?",
|
||||
"playable_button_title": "Показати лише ігри, які можна грати зараз"
|
||||
},
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"language_name": "中文",
|
||||
"language_name": "简体中文",
|
||||
"app": {
|
||||
"successfully_signed_in": "已成功登录"
|
||||
},
|
||||
@@ -26,7 +26,9 @@
|
||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||
"sign_in": "登入",
|
||||
"friends": "好友",
|
||||
"favorites": "收藏"
|
||||
"favorites": "收藏",
|
||||
"need_help": "需要帮助?",
|
||||
"playable_button_title": "仅显示现在可以游玩的游戏"
|
||||
},
|
||||
"header": {
|
||||
"search": "搜索游戏",
|
||||
@@ -43,11 +45,23 @@
|
||||
"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": "上一页"
|
||||
"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 +180,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 +254,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 +321,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 +408,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 +497,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 +506,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 +526,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 +543,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": "了解更多"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { app } from "electron";
|
||||
import path from "node:path";
|
||||
import { SystemPath } from "./services/system-path";
|
||||
|
||||
export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
|
||||
|
||||
export const defaultDownloadsPath = SystemPath.getPath("downloads");
|
||||
|
||||
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
||||
@@ -16,6 +14,8 @@ export const windowsStartMenuPath = path.join(
|
||||
"Programs"
|
||||
);
|
||||
|
||||
export const publicProfilePath = "C:/Users/Public";
|
||||
|
||||
export const levelDatabasePath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
`hydra-db${isStaging ? "-staging" : ""}`
|
||||
@@ -26,11 +26,10 @@ export const commonRedistPath = path.join(
|
||||
"CommonRedist"
|
||||
);
|
||||
|
||||
export const logsPath = path.join(SystemPath.getPath("userData"), "logs");
|
||||
|
||||
export const seedsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "seeds")
|
||||
: path.join(__dirname, "..", "..", "seeds");
|
||||
export const logsPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
`logs${isStaging ? "-staging" : ""}`
|
||||
);
|
||||
|
||||
export const achievementSoundPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "achievement.wav")
|
||||
@@ -40,4 +39,6 @@ export const backupsPath = path.join(SystemPath.getPath("userData"), "Backups");
|
||||
|
||||
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
||||
|
||||
export const MAIN_LOOP_INTERVAL = 1500;
|
||||
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
|
||||
|
||||
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,9 @@ 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);
|
||||
return gamesShopAssetsSublevel.put(key, { ...existingAssets, ...assets });
|
||||
};
|
||||
|
||||
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
||||
|
||||
@@ -1,74 +1,93 @@
|
||||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
||||
import { CloudSync, HydraApi, logger, WindowManager } from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import * as tar from "tar";
|
||||
import { registerEvent } from "../register-event";
|
||||
import axios from "axios";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import type { GameShop } from "@types";
|
||||
import { backupsPath, publicProfilePath } from "@main/constants";
|
||||
import type { GameShop, LudusaviBackupMapping } from "@types";
|
||||
|
||||
import YAML from "yaml";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { addTrailingSlash, normalizePath } from "@main/helpers";
|
||||
import { SystemPath } from "@main/services/system-path";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export interface LudusaviBackup {
|
||||
files: {
|
||||
[key: string]: {
|
||||
hash: string;
|
||||
size: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
export const transformLudusaviBackupPathIntoWindowsPath = (
|
||||
backupPath: string,
|
||||
winePrefixPath?: string | null
|
||||
) => {
|
||||
return backupPath
|
||||
.replace(winePrefixPath ? addTrailingSlash(winePrefixPath) : "", "")
|
||||
.replace("drive_c", "C:");
|
||||
};
|
||||
|
||||
const replaceLudusaviBackupWithCurrentUser = (
|
||||
export const addWinePrefixToWindowsPath = (
|
||||
windowsPath: string,
|
||||
winePrefixPath?: string | null
|
||||
) => {
|
||||
if (!winePrefixPath) {
|
||||
return windowsPath;
|
||||
}
|
||||
|
||||
return path.join(winePrefixPath, windowsPath.replace("C:", "drive_c"));
|
||||
};
|
||||
|
||||
const restoreLudusaviBackup = (
|
||||
backupPath: string,
|
||||
title: string,
|
||||
homeDir: string
|
||||
homeDir: string,
|
||||
winePrefixPath?: string | null,
|
||||
artifactWinePrefixPath?: string | null
|
||||
) => {
|
||||
const gameBackupPath = path.join(backupPath, title);
|
||||
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
|
||||
|
||||
const data = fs.readFileSync(mappingYamlPath, "utf8");
|
||||
const manifest = YAML.parse(data) as {
|
||||
backups: LudusaviBackup[];
|
||||
backups: LudusaviBackupMapping[];
|
||||
drives: Record<string, string>;
|
||||
};
|
||||
|
||||
const currentHomeDir = normalizePath(SystemPath.getPath("home"));
|
||||
const userProfilePath =
|
||||
CloudSync.getWindowsLikeUserProfilePath(winePrefixPath);
|
||||
|
||||
/* Renaming logic */
|
||||
if (os.platform() === "win32") {
|
||||
const mappedHomeDir = path.join(
|
||||
gameBackupPath,
|
||||
path.join("drive-C", homeDir.replace("C:", ""))
|
||||
);
|
||||
|
||||
if (fs.existsSync(mappedHomeDir)) {
|
||||
fs.renameSync(
|
||||
mappedHomeDir,
|
||||
path.join(gameBackupPath, "drive-C", currentHomeDir.replace("C:", ""))
|
||||
manifest.backups.forEach((backup) => {
|
||||
Object.keys(backup.files).forEach((key) => {
|
||||
const sourcePathWithDrives = Object.entries(manifest.drives).reduce(
|
||||
(prev, [driveKey, driveValue]) => {
|
||||
return prev.replace(driveValue, driveKey);
|
||||
},
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const backups = manifest.backups.map((backup: LudusaviBackup) => {
|
||||
const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
|
||||
const updatedKey = key.replace(homeDir, currentHomeDir);
|
||||
const sourcePath = path.join(gameBackupPath, sourcePathWithDrives);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[updatedKey]: value,
|
||||
};
|
||||
}, {});
|
||||
logger.info(`Source path: ${sourcePath}`);
|
||||
|
||||
return {
|
||||
...backup,
|
||||
files,
|
||||
};
|
||||
const destinationPath = transformLudusaviBackupPathIntoWindowsPath(
|
||||
key,
|
||||
artifactWinePrefixPath
|
||||
)
|
||||
.replace(
|
||||
homeDir,
|
||||
addWinePrefixToWindowsPath(userProfilePath, winePrefixPath)
|
||||
)
|
||||
.replace(
|
||||
publicProfilePath,
|
||||
addWinePrefixToWindowsPath(publicProfilePath, winePrefixPath)
|
||||
);
|
||||
|
||||
logger.info(`Moving ${sourcePath} to ${destinationPath}`);
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
fs.unlinkSync(destinationPath);
|
||||
}
|
||||
|
||||
fs.renameSync(sourcePath, destinationPath);
|
||||
});
|
||||
});
|
||||
|
||||
fs.writeFileSync(mappingYamlPath, YAML.stringify({ ...manifest, backups }));
|
||||
};
|
||||
|
||||
const downloadGameArtifact = async (
|
||||
@@ -78,10 +97,18 @@ const downloadGameArtifact = async (
|
||||
gameArtifactId: string
|
||||
) => {
|
||||
try {
|
||||
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const {
|
||||
downloadUrl,
|
||||
objectKey,
|
||||
homeDir,
|
||||
winePrefixPath: artifactWinePrefixPath,
|
||||
} = await HydraApi.post<{
|
||||
downloadUrl: string;
|
||||
objectKey: string;
|
||||
homeDir: string;
|
||||
winePrefixPath: string | null;
|
||||
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
|
||||
|
||||
const zipLocation = path.join(SystemPath.getPath("userData"), objectKey);
|
||||
@@ -109,34 +136,34 @@ const downloadGameArtifact = async (
|
||||
response.data.pipe(writer);
|
||||
|
||||
writer.on("error", (err) => {
|
||||
logger.error("Failed to write zip", err);
|
||||
logger.error("Failed to write tar file", err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
fs.mkdirSync(backupPath, { recursive: true });
|
||||
|
||||
writer.on("close", () => {
|
||||
tar
|
||||
.x({
|
||||
file: zipLocation,
|
||||
cwd: backupPath,
|
||||
})
|
||||
.then(async () => {
|
||||
replaceLudusaviBackupWithCurrentUser(
|
||||
backupPath,
|
||||
objectId,
|
||||
normalizePath(homeDir)
|
||||
);
|
||||
writer.on("close", async () => {
|
||||
await tar.x({
|
||||
file: zipLocation,
|
||||
cwd: backupPath,
|
||||
});
|
||||
|
||||
Ludusavi.restoreBackup(backupPath).then(() => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
restoreLudusaviBackup(
|
||||
backupPath,
|
||||
objectId,
|
||||
normalizePath(homeDir),
|
||||
game?.winePrefixPath,
|
||||
artifactWinePrefixPath
|
||||
);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Failed to download game artifact", err);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
false
|
||||
|
||||
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);
|
||||
@@ -16,6 +16,8 @@ import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/add-game-to-favorites";
|
||||
import "./library/remove-game-from-favorites";
|
||||
import "./library/add-game-to-pinned";
|
||||
import "./library/remove-game-from-pinned";
|
||||
import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
@@ -33,7 +35,10 @@ 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/create-steam-shortcut";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
@@ -61,6 +66,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";
|
||||
@@ -84,7 +90,11 @@ import "./cloud-save/get-game-backup-preview";
|
||||
import "./cloud-save/upload-save-game";
|
||||
import "./cloud-save/delete-game-artifact";
|
||||
import "./cloud-save/select-game-backup-path";
|
||||
import "./cloud-save/toggle-artifact-freeze";
|
||||
import "./cloud-save/rename-game-artifact";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import "./notifications/update-achievement-notification-window";
|
||||
import "./notifications/show-achievement-test-notification";
|
||||
import "./themes/add-custom-theme";
|
||||
import "./themes/delete-custom-theme";
|
||||
import "./themes/get-all-custom-themes";
|
||||
|
||||
@@ -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,
|
||||
@@ -43,7 +43,10 @@ const addGameToLibrary = async (
|
||||
|
||||
await createGame(game).catch(() => {});
|
||||
|
||||
updateLocalUnlockedAchievements(game);
|
||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("addGameToLibrary", addGameToLibrary);
|
||||
|
||||
29
src/main/events/library/add-game-to-pinned.ts
Normal file
29
src/main/events/library/add-game-to-pinned.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const addGameToPinned = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
const response = await HydraApi.put(`/profile/games/${shop}/${objectId}/pin`);
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
pinned: true,
|
||||
pinnedDate: new Date(response.pinnedDate),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update game pinned status: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("addGameToPinned", addGameToPinned);
|
||||
29
src/main/events/library/change-game-playtime.ts
Normal file
29
src/main/events/library/change-game-playtime.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
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 favorite status: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("changeGamePlayTime", changeGamePlaytime);
|
||||
181
src/main/events/library/create-steam-shortcut.ts
Normal file
181
src/main/events/library/create-steam-shortcut.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameShop, GameStats } from "@types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import {
|
||||
composeSteamShortcut,
|
||||
getSteamLocation,
|
||||
getSteamShortcuts,
|
||||
getSteamUsersIds,
|
||||
HydraApi,
|
||||
logger,
|
||||
SystemPath,
|
||||
writeSteamShortcuts,
|
||||
} from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import axios from "axios";
|
||||
import path from "node:path";
|
||||
import { ASSETS_PATH } from "@main/constants";
|
||||
|
||||
const downloadAsset = async (downloadPath: string, url?: string | null) => {
|
||||
try {
|
||||
if (fs.existsSync(downloadPath)) {
|
||||
return downloadPath;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(downloadPath), { recursive: true });
|
||||
|
||||
const response = await axios.get(url, { responseType: "arraybuffer" });
|
||||
fs.writeFileSync(downloadPath, response.data);
|
||||
|
||||
return downloadPath;
|
||||
} catch (error) {
|
||||
logger.error("Failed to download asset", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAssetsFromSteam = async (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
assets: GameStats["assets"]
|
||||
) => {
|
||||
const gameAssetsPath = path.join(ASSETS_PATH, `${shop}-${objectId}`);
|
||||
|
||||
return await Promise.all([
|
||||
downloadAsset(path.join(gameAssetsPath, "icon.ico"), assets?.iconUrl),
|
||||
downloadAsset(
|
||||
path.join(gameAssetsPath, "hero.jpg"),
|
||||
assets?.libraryHeroImageUrl
|
||||
),
|
||||
downloadAsset(path.join(gameAssetsPath, "logo.png"), assets?.logoImageUrl),
|
||||
downloadAsset(
|
||||
path.join(gameAssetsPath, "cover.jpg"),
|
||||
assets?.coverImageUrl
|
||||
),
|
||||
downloadAsset(
|
||||
path.join(gameAssetsPath, "library.jpg"),
|
||||
assets?.libraryImageUrl
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
const copyAssetIfExists = async (
|
||||
sourcePath: string | null,
|
||||
destinationPath: string
|
||||
) => {
|
||||
if (sourcePath && fs.existsSync(sourcePath)) {
|
||||
logger.info("Copying Steam asset", sourcePath, destinationPath);
|
||||
await fs.promises.cp(sourcePath, destinationPath);
|
||||
}
|
||||
};
|
||||
|
||||
const createSteamShortcut = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
if (!game.executablePath) {
|
||||
throw new Error("No executable path found for game");
|
||||
}
|
||||
|
||||
const { assets } = await HydraApi.get<GameStats>(
|
||||
`/games/stats?objectId=${objectId}&shop=${shop}`
|
||||
);
|
||||
|
||||
const steamUserIds = await getSteamUsersIds();
|
||||
|
||||
if (!steamUserIds.length) {
|
||||
logger.error("No Steam user ID found");
|
||||
throw new Error("No Steam user ID found");
|
||||
}
|
||||
|
||||
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =
|
||||
await downloadAssetsFromSteam(game.shop, game.objectId, assets);
|
||||
|
||||
const newShortcut = composeSteamShortcut(
|
||||
game.title,
|
||||
game.executablePath,
|
||||
iconImage
|
||||
);
|
||||
|
||||
for (const steamUserId of steamUserIds) {
|
||||
logger.info("Adding shortcut for Steam user", steamUserId);
|
||||
|
||||
const steamShortcuts = await getSteamShortcuts(steamUserId);
|
||||
|
||||
if (steamShortcuts.some((shortcut) => shortcut.appname === game.title)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const gridPath = path.join(
|
||||
await getSteamLocation(),
|
||||
"userdata",
|
||||
steamUserId.toString(),
|
||||
"config",
|
||||
"grid"
|
||||
);
|
||||
|
||||
await fs.promises.mkdir(gridPath, { recursive: true });
|
||||
|
||||
await Promise.all([
|
||||
copyAssetIfExists(
|
||||
heroImage,
|
||||
path.join(gridPath, `${newShortcut.appid}_hero.jpg`)
|
||||
),
|
||||
copyAssetIfExists(
|
||||
logoImage,
|
||||
path.join(gridPath, `${newShortcut.appid}_logo.png`)
|
||||
),
|
||||
copyAssetIfExists(
|
||||
coverImage,
|
||||
path.join(gridPath, `${newShortcut.appid}p.jpg`)
|
||||
),
|
||||
copyAssetIfExists(
|
||||
libraryImage,
|
||||
path.join(gridPath, `${newShortcut.appid}.jpg`)
|
||||
),
|
||||
]);
|
||||
|
||||
steamShortcuts.push(newShortcut);
|
||||
|
||||
logger.info(newShortcut);
|
||||
logger.info("Writing Steam shortcuts", steamShortcuts);
|
||||
|
||||
await writeSteamShortcuts(steamUserId, steamShortcuts);
|
||||
}
|
||||
|
||||
if (process.platform === "linux" && !game.winePrefixPath) {
|
||||
const steamWinePrefixes = path.join(
|
||||
SystemPath.getPath("home"),
|
||||
".local",
|
||||
"share",
|
||||
"Steam",
|
||||
"steamapps",
|
||||
"compatdata"
|
||||
);
|
||||
|
||||
const winePrefixPath = path.join(
|
||||
steamWinePrefixes,
|
||||
newShortcut.appid.toString(),
|
||||
"pfx"
|
||||
);
|
||||
|
||||
await fs.promises.mkdir(winePrefixPath, { recursive: true });
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
winePrefixPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("createSteamShortcut", createSteamShortcut);
|
||||
@@ -0,0 +1,30 @@
|
||||
import { logger, SystemPath } from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDefaultWinePrefixSelectionPath = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
try {
|
||||
const steamWinePrefixes = path.join(
|
||||
SystemPath.getPath("home"),
|
||||
".local",
|
||||
"share",
|
||||
"Steam",
|
||||
"steamapps",
|
||||
"compatdata"
|
||||
);
|
||||
|
||||
return await fs.promises.realpath(steamWinePrefixes);
|
||||
} catch (err) {
|
||||
logger.error("Failed to get default wine prefix selection path", err);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"getDefaultWinePrefixSelectionPath",
|
||||
getDefaultWinePrefixSelectionPath
|
||||
);
|
||||
@@ -12,16 +12,14 @@ const openGameInstallerPath = async (
|
||||
) => {
|
||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!download || !download.folderName || !download.downloadPath) return true;
|
||||
if (!download?.folderName || !download.downloadPath) return;
|
||||
|
||||
const gamePath = path.join(
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName!
|
||||
download.folderName
|
||||
);
|
||||
|
||||
shell.showItemInFolder(gamePath);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
registerEvent("openGameInstallerPath", openGameInstallerPath);
|
||||
|
||||
@@ -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,
|
||||
|
||||
29
src/main/events/library/remove-game-from-pinned.ts
Normal file
29
src/main/events/library/remove-game-from-pinned.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const removeGameFromPinned = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`).catch(() => {});
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
pinned: false,
|
||||
pinnedDate: null,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update game pinned status: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("removeGameFromPinned", removeGameFromPinned);
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import fs from "node:fs";
|
||||
import { levelKeys, gamesSublevel } from "@main/level";
|
||||
import { Wine } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const selectGameWinePrefix = async (
|
||||
@@ -14,9 +16,24 @@ const selectGameWinePrefix = async (
|
||||
|
||||
if (!game) return;
|
||||
|
||||
if (!winePrefixPath) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
winePrefixPath: null,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const realWinePrefixPath = await fs.promises.realpath(winePrefixPath);
|
||||
|
||||
if (!Wine.validatePrefix(realWinePrefixPath)) {
|
||||
throw new Error("Invalid wine prefix path");
|
||||
}
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
winePrefixPath: winePrefixPath,
|
||||
winePrefixPath: realWinePrefixPath,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ const verifyExecutablePathInUse = async (
|
||||
) => {
|
||||
for await (const game of gamesSublevel.values()) {
|
||||
if (game.executablePath === executablePath) {
|
||||
return true;
|
||||
return game;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
};
|
||||
|
||||
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const showAchievementTestNotification = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
setTimeout(() => {
|
||||
WindowManager.showAchievementTestNotification();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"showAchievementTestNotification",
|
||||
showAchievementTestNotification
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
import { UserPreferences } from "@types";
|
||||
|
||||
const updateAchievementCustomNotificationWindow = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
WindowManager.closeNotificationWindow();
|
||||
|
||||
if (
|
||||
userPreferences.achievementNotificationsEnabled !== false &&
|
||||
userPreferences.achievementCustomNotificationsEnabled !== false
|
||||
) {
|
||||
WindowManager.createNotificationWindow();
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"updateAchievementCustomNotificationWindow",
|
||||
updateAchievementCustomNotificationWindow
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { themesSublevel } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const toggleCustomTheme = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -17,6 +18,8 @@ const toggleCustomTheme = async (
|
||||
isActive,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
WindowManager.notificationWindow?.webContents.send("on-custom-theme-updated");
|
||||
};
|
||||
|
||||
registerEvent("toggleCustomTheme", toggleCustomTheme);
|
||||
|
||||
@@ -20,7 +20,10 @@ const updateCustomTheme = async (
|
||||
});
|
||||
|
||||
if (theme.isActive) {
|
||||
WindowManager.mainWindow?.webContents.send("css-injected", code);
|
||||
WindowManager.mainWindow?.webContents.send("on-custom-theme-updated");
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-custom-theme-updated"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
23
src/main/events/user/get-user-library.ts
Normal file
23
src/main/events/user/get-user-library.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
): Promise<UserLibraryResponse | null> => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
params.append("take", take.toString());
|
||||
params.append("skip", skip.toString());
|
||||
|
||||
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);
|
||||
@@ -32,3 +32,8 @@ export const isPortableVersion = () => {
|
||||
|
||||
export const normalizePath = (str: string) =>
|
||||
path.posix.normalize(str).replace(/\\/g, "/");
|
||||
|
||||
export const addTrailingSlash = (str: string) =>
|
||||
str.endsWith("/") ? str : `${str}/`;
|
||||
|
||||
export * from "./reg-parser";
|
||||
|
||||
58
src/main/helpers/reg-parser.ts
Normal file
58
src/main/helpers/reg-parser.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
type RegValue = string | number | null;
|
||||
|
||||
interface RegEntry {
|
||||
path: string;
|
||||
timestamp?: string;
|
||||
values: Record<string, RegValue>;
|
||||
}
|
||||
|
||||
export function parseRegFile(content: string): RegEntry[] {
|
||||
const lines = content.split(/\r?\n/);
|
||||
const entries: RegEntry[] = [];
|
||||
|
||||
let currentPath: string | null = null;
|
||||
let currentEntry: RegEntry | null = null;
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith(";") || line.startsWith(";;")) continue;
|
||||
|
||||
if (line.startsWith("#")) {
|
||||
const match = line.match(/^#time=(\w+)/);
|
||||
if (match && currentEntry) {
|
||||
currentEntry.timestamp = match[1];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("[")) {
|
||||
const match = line.match(/^\[(.+?)\](?:\s+\d+)?/);
|
||||
if (match) {
|
||||
if (currentEntry) entries.push(currentEntry);
|
||||
currentPath = match[1];
|
||||
currentEntry = { path: currentPath, values: {} };
|
||||
}
|
||||
} else if (currentEntry) {
|
||||
const kvMatch = line.match(/^"?(.*?)"?=(.*)$/);
|
||||
if (kvMatch) {
|
||||
const [, key, rawValue] = kvMatch;
|
||||
let value: RegValue;
|
||||
|
||||
if (rawValue === '""') {
|
||||
value = "";
|
||||
} else if (rawValue.startsWith("dword:")) {
|
||||
value = parseInt(rawValue.slice(6), 16);
|
||||
} else if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
|
||||
value = rawValue.slice(1, -1);
|
||||
} else {
|
||||
value = rawValue;
|
||||
}
|
||||
|
||||
currentEntry.values[key || "@"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentEntry) entries.push(currentEntry);
|
||||
return entries;
|
||||
}
|
||||
@@ -4,7 +4,12 @@ import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import {
|
||||
logger,
|
||||
clearGamesPlaytime,
|
||||
WindowManager,
|
||||
Lock,
|
||||
} from "@main/services";
|
||||
import resources from "@locales";
|
||||
import { PythonRPC } from "./services/python-rpc";
|
||||
import { db, levelKeys } from "./level";
|
||||
@@ -23,7 +28,9 @@ autoUpdater.logger = logger;
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
|
||||
app.commandLine.appendSwitch("--no-sandbox");
|
||||
if (process.platform !== "linux") {
|
||||
app.commandLine.appendSwitch("--no-sandbox");
|
||||
}
|
||||
|
||||
i18n.init({
|
||||
resources,
|
||||
@@ -71,6 +78,7 @@ app.whenReady().then(async () => {
|
||||
WindowManager.createMainWindow();
|
||||
}
|
||||
|
||||
WindowManager.createNotificationWindow();
|
||||
WindowManager.createSystemTray(language || "en");
|
||||
});
|
||||
|
||||
@@ -140,9 +148,19 @@ app.on("window-all-closed", () => {
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
/* Disconnects libtorrent */
|
||||
PythonRPC.kill();
|
||||
let canAppBeClosed = false;
|
||||
|
||||
app.on("before-quit", async (e) => {
|
||||
await Lock.releaseLock();
|
||||
|
||||
if (!canAppBeClosed) {
|
||||
e.preventDefault();
|
||||
/* Disconnects libtorrent */
|
||||
PythonRPC.kill();
|
||||
await clearGamesPlaytime();
|
||||
canAppBeClosed = true;
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
|
||||
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,21 +4,21 @@ import { Downloader } from "@shared";
|
||||
import { levelKeys, db } from "./level";
|
||||
import type { UserPreferences } from "@types";
|
||||
import {
|
||||
WSClient,
|
||||
SystemPath,
|
||||
CommonRedistManager,
|
||||
TorBoxClient,
|
||||
RealDebridClient,
|
||||
Aria2,
|
||||
DownloadManager,
|
||||
Ludusavi,
|
||||
HydraApi,
|
||||
uploadGamesBatch,
|
||||
startMainLoop,
|
||||
Ludusavi,
|
||||
Lock,
|
||||
} from "@main/services";
|
||||
|
||||
export const loadState = async () => {
|
||||
SystemPath.checkIfPathsAreAvailable();
|
||||
await Lock.acquireLock();
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
@@ -29,7 +29,9 @@ export const loadState = async () => {
|
||||
|
||||
await import("./events");
|
||||
|
||||
Aria2.spawn();
|
||||
if (process.platform !== "darwin") {
|
||||
Aria2.spawn();
|
||||
}
|
||||
|
||||
if (userPreferences?.realDebridApiToken) {
|
||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||
@@ -39,11 +41,12 @@ export const loadState = async () => {
|
||||
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||
}
|
||||
|
||||
Ludusavi.addManifestToLudusaviConfig();
|
||||
Ludusavi.copyConfigFileToUserData();
|
||||
Ludusavi.copyBinaryToUserData();
|
||||
|
||||
await HydraApi.setupApi().then(() => {
|
||||
uploadGamesBatch();
|
||||
WSClient.connect();
|
||||
// WSClient.connect();
|
||||
});
|
||||
|
||||
const downloads = await downloadsSublevel
|
||||
@@ -77,4 +80,6 @@ export const loadState = async () => {
|
||||
startMainLoop();
|
||||
|
||||
CommonRedistManager.downloadCommonRedist();
|
||||
|
||||
SystemPath.checkIfPathsAreAvailable();
|
||||
};
|
||||
|
||||
@@ -3,15 +3,24 @@ import { mergeAchievements } from "./merge-achievements";
|
||||
import fs, { readdirSync } from "node:fs";
|
||||
import {
|
||||
findAchievementFileInExecutableDirectory,
|
||||
findAchievementFileInSteamPath,
|
||||
findAchievementFiles,
|
||||
findAllAchievementFiles,
|
||||
getAlternativeObjectIds,
|
||||
} from "./find-achivement-files";
|
||||
import type { AchievementFile, Game, UnlockedAchievement } from "@types";
|
||||
import type {
|
||||
AchievementFile,
|
||||
Game,
|
||||
GameShop,
|
||||
UnlockedAchievement,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { Cracker } from "@shared";
|
||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
|
||||
const fileStats: Map<string, number> = new Map();
|
||||
const fltFiles: Map<string, Set<string>> = new Map();
|
||||
@@ -30,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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -53,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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -120,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;
|
||||
}
|
||||
@@ -129,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();
|
||||
@@ -181,10 +243,14 @@ export class AchievementWatcherManager {
|
||||
}
|
||||
}
|
||||
|
||||
return mergeAchievements(game, unlockedAchievements, false);
|
||||
if (unlockedAchievements.length) {
|
||||
return mergeAchievements(game, unlockedAchievements, false);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static preSearchAchievementsWindows = async () => {
|
||||
private static async getGameAchievementFilesWindows() {
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
@@ -193,68 +259,104 @@ export class AchievementWatcherManager {
|
||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
const gameAchievementFiles: AchievementFile[] = [];
|
||||
games.map(async (game) => {
|
||||
const achievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
gameAchievementFiles.push(
|
||||
achievementFiles.push(
|
||||
...(gameAchievementFilesMap.get(objectId) || [])
|
||||
);
|
||||
|
||||
gameAchievementFiles.push(
|
||||
achievementFiles.push(
|
||||
...findAchievementFileInExecutableDirectory(game)
|
||||
);
|
||||
|
||||
achievementFiles.push(
|
||||
...(await findAchievementFileInSteamPath(game))
|
||||
);
|
||||
}
|
||||
|
||||
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
||||
return { game, achievementFiles };
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static preSearchAchievementsWithWine = async () => {
|
||||
private static async getGameAchievementFilesLinux() {
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => games.filter((game) => !game.isDeleted));
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
games.map(async (game) => {
|
||||
const achievementFiles = findAchievementFiles(game);
|
||||
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
achievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||
|
||||
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
||||
return { game, achievementFiles };
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static async notifyCombinedAchievementsUnlocked(
|
||||
totalNewGamesWithAchievements: number,
|
||||
totalNewAchievements: number
|
||||
) {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-combined-achievements-unlocked",
|
||||
totalNewGamesWithAchievements,
|
||||
totalNewAchievements,
|
||||
userPreferences.achievementCustomNotificationPosition ?? "top-left"
|
||||
);
|
||||
} else {
|
||||
publishCombinedNewAchievementNotification(
|
||||
totalNewAchievements,
|
||||
totalNewGamesWithAchievements
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async preSearchAchievements() {
|
||||
try {
|
||||
const newAchievementsCount =
|
||||
const gameAchievementFiles =
|
||||
process.platform === "win32"
|
||||
? await this.preSearchAchievementsWindows()
|
||||
: await this.preSearchAchievementsWithWine();
|
||||
? await this.getGameAchievementFilesWindows()
|
||||
: await this.getGameAchievementFilesLinux();
|
||||
|
||||
const newAchievementsCount = await Promise.all(
|
||||
gameAchievementFiles.map(({ game, achievementFiles }) => {
|
||||
return this.preProcessGameAchievementFiles(game, achievementFiles);
|
||||
})
|
||||
);
|
||||
|
||||
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
||||
(achievements) => achievements
|
||||
).length;
|
||||
|
||||
const totalNewAchievements = newAchievementsCount.reduce(
|
||||
(acc, val) => acc + val,
|
||||
0
|
||||
);
|
||||
|
||||
if (totalNewAchievements > 0) {
|
||||
publishCombinedNewAchievementNotification(
|
||||
totalNewAchievements,
|
||||
totalNewGamesWithAchievements
|
||||
await setTimeout(4000);
|
||||
this.notifyCombinedAchievementsUnlocked(
|
||||
totalNewGamesWithAchievements,
|
||||
totalNewAchievements
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
achievementsLogger.error("Error on preSearchAchievements", err);
|
||||
}
|
||||
|
||||
this.hasFinishedMergingWithRemote = true;
|
||||
this._hasFinishedPreSearch = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 +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: Date.now() + 1000 * 60 * 30, // 30 minutes
|
||||
updatedAt: Date.now() + LOCAL_CACHE_EXPIRATION,
|
||||
});
|
||||
|
||||
return achievements;
|
||||
@@ -48,8 +69,14 @@ export const getGameAchievementData = async (
|
||||
throw err;
|
||||
}
|
||||
|
||||
const isNotModified = (err as AxiosError)?.response?.status === 304;
|
||||
|
||||
if (isNotModified) {
|
||||
return cachedAchievements?.achievements ?? [];
|
||||
}
|
||||
|
||||
logger.error("Failed to get game achievements for", objectId, err);
|
||||
|
||||
return [];
|
||||
return cachedAchievements?.achievements ?? [];
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AchievementNotificationInfo,
|
||||
Game,
|
||||
GameShop,
|
||||
UnlockedAchievement,
|
||||
@@ -12,6 +13,14 @@ import { publishNewAchievementNotification } from "../notifications";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { getGameAchievementData } from "./get-game-achievement-data";
|
||||
import { AchievementWatcherManager } from "./achievement-watcher-manager";
|
||||
|
||||
const isRareAchievement = (points: number) => {
|
||||
const rawPercentage = (50 - Math.sqrt(points)) * 2;
|
||||
|
||||
return rawPercentage < 10;
|
||||
};
|
||||
|
||||
const saveAchievementsOnLocal = async (
|
||||
objectId: string,
|
||||
@@ -27,7 +36,7 @@ const saveAchievementsOnLocal = async (
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
achievements: gameAchievement?.achievements ?? [],
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
cacheExpiresTimestamp: gameAchievement?.cacheExpiresTimestamp,
|
||||
updatedAt: gameAchievement?.updatedAt,
|
||||
});
|
||||
|
||||
if (!sendUpdateEvent) return;
|
||||
@@ -48,12 +57,20 @@ export const mergeAchievements = async (
|
||||
achievements: UnlockedAchievement[],
|
||||
publishNotification: boolean
|
||||
) => {
|
||||
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectId)),
|
||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||
const gameKey = levelKeys.game(game.shop, game.objectId);
|
||||
|
||||
let localGameAchievement = await gameAchievementsSublevel.get(gameKey);
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
if (!localGameAchievement) {
|
||||
await getGameAchievementData(game.objectId, game.shop, false);
|
||||
localGameAchievement = await gameAchievementsSublevel.get(gameKey);
|
||||
}
|
||||
|
||||
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
||||
@@ -84,9 +101,9 @@ export const mergeAchievements = async (
|
||||
if (
|
||||
newAchievements.length &&
|
||||
publishNotification &&
|
||||
userPreferences?.achievementNotificationsEnabled
|
||||
userPreferences.achievementNotificationsEnabled !== false
|
||||
) {
|
||||
const achievementsInfo = newAchievements
|
||||
const filteredAchievements = newAchievements
|
||||
.toSorted((a, b) => {
|
||||
return a.unlockTime - b.unlockTime;
|
||||
})
|
||||
@@ -98,24 +115,54 @@ export const mergeAchievements = async (
|
||||
);
|
||||
});
|
||||
})
|
||||
.filter((achievement) => Boolean(achievement))
|
||||
.map((achievement) => {
|
||||
.filter((achievement) => !!achievement);
|
||||
|
||||
const achievementsInfo: AchievementNotificationInfo[] =
|
||||
filteredAchievements.map((achievement, index) => {
|
||||
return {
|
||||
displayName: achievement!.displayName,
|
||||
iconUrl: achievement!.icon,
|
||||
title: achievement.displayName,
|
||||
description: achievement.description,
|
||||
points: achievement.points,
|
||||
isHidden: achievement.hidden,
|
||||
isRare: achievement.points
|
||||
? isRareAchievement(achievement.points)
|
||||
: false,
|
||||
isPlatinum:
|
||||
index === filteredAchievements.length - 1 &&
|
||||
newAchievements.length + unlockedAchievements.length ===
|
||||
achievementsData.length,
|
||||
iconUrl: achievement.icon,
|
||||
};
|
||||
});
|
||||
|
||||
publishNewAchievementNotification({
|
||||
achievements: achievementsInfo,
|
||||
unlockedAchievementCount: mergedLocalAchievements.length,
|
||||
totalAchievementCount: achievementsData.length,
|
||||
gameTitle: game.title,
|
||||
gameIcon: game.iconUrl,
|
||||
});
|
||||
achievementsLogger.log(
|
||||
"Publishing achievement notification",
|
||||
game.objectId,
|
||||
game.title
|
||||
);
|
||||
|
||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
userPreferences.achievementCustomNotificationPosition ?? "top-left",
|
||||
achievementsInfo
|
||||
);
|
||||
} else {
|
||||
publishNewAchievementNotification({
|
||||
achievements: achievementsInfo,
|
||||
unlockedAchievementCount: mergedLocalAchievements.length,
|
||||
totalAchievementCount: achievementsData.length,
|
||||
gameTitle: game.title,
|
||||
gameIcon: game.iconUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (game.remoteId) {
|
||||
const shouldSyncWithRemote =
|
||||
game.remoteId &&
|
||||
(newAchievements.length || AchievementWatcherManager.hasFinishedPreSearch);
|
||||
|
||||
if (shouldSyncWithRemote) {
|
||||
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
|
||||
"/profile/games/achievements",
|
||||
{
|
||||
@@ -156,8 +203,11 @@ export const mergeAchievements = async (
|
||||
mergedLocalAchievements,
|
||||
publishNotification
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
AchievementWatcherManager.alreadySyncedGames.set(gameKey, true);
|
||||
});
|
||||
} else {
|
||||
} else if (newAchievements.length) {
|
||||
await saveAchievementsOnLocal(
|
||||
game.objectId,
|
||||
game.shop,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -7,8 +7,8 @@ export class Aria2 {
|
||||
|
||||
public static spawn() {
|
||||
const binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
? path.join(process.resourcesPath, "aria2c")
|
||||
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
||||
|
||||
this.process = cp.spawn(
|
||||
binaryPath,
|
||||
|
||||
@@ -7,7 +7,7 @@ import os from "node:os";
|
||||
import type { GameShop, User } from "@types";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { normalizePath, parseRegFile } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
@@ -17,6 +17,39 @@ import i18next, { t } from "i18next";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class CloudSync {
|
||||
public static getWindowsLikeUserProfilePath(winePrefixPath?: string | null) {
|
||||
if (process.platform === "linux") {
|
||||
if (!winePrefixPath) {
|
||||
throw new Error("Wine prefix path is required");
|
||||
}
|
||||
|
||||
const userReg = fs.readFileSync(
|
||||
path.join(winePrefixPath, "user.reg"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const entries = parseRegFile(userReg);
|
||||
const volatileEnvironment = entries.find(
|
||||
(entry) => entry.path === "Volatile Environment"
|
||||
);
|
||||
|
||||
if (!volatileEnvironment) {
|
||||
throw new Error("Volatile environment not found in user.reg");
|
||||
}
|
||||
|
||||
const { values } = volatileEnvironment;
|
||||
const userProfile = String(values["USERPROFILE"]);
|
||||
|
||||
if (userProfile) {
|
||||
return normalizePath(userProfile);
|
||||
} else {
|
||||
throw new Error("User profile not found in user.reg");
|
||||
}
|
||||
}
|
||||
|
||||
return normalizePath(SystemPath.getPath("home"));
|
||||
}
|
||||
|
||||
public static getBackupLabel(automatic: boolean) {
|
||||
const language = i18next.language;
|
||||
|
||||
@@ -102,9 +135,12 @@ export class CloudSync {
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(SystemPath.getPath("home")),
|
||||
winePrefixPath: game?.winePrefixPath
|
||||
? fs.realpathSync(game.winePrefixPath)
|
||||
: null,
|
||||
homeDir: this.getWindowsLikeUserProfilePath(game?.winePrefixPath ?? null),
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
platform: process.platform,
|
||||
label,
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -42,7 +42,7 @@ export class HydraApi {
|
||||
subscription: null,
|
||||
};
|
||||
|
||||
private static isLoggedIn() {
|
||||
public static isLoggedIn() {
|
||||
return this.userAuth.authToken !== "";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -15,3 +15,5 @@ export * from "./aria2";
|
||||
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,14 +31,16 @@ 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,
|
||||
pinned: game.isPinned ?? localGame.pinned,
|
||||
});
|
||||
} else {
|
||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
objectId: game.objectId,
|
||||
title: game.title,
|
||||
remoteId: game.id,
|
||||
@@ -44,24 +48,24 @@ export const mergeWithRemoteGames = async () => {
|
||||
iconUrl: game.iconUrl,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime,
|
||||
isDeleted: false,
|
||||
favorite: game.isFavorite ?? false,
|
||||
pinned: 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: game.title,
|
||||
coverImageUrl: game.coverImageUrl,
|
||||
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||
libraryImageUrl: game.libraryImageUrl,
|
||||
logoImageUrl: game.logoImageUrl,
|
||||
iconUrl: game.iconUrl,
|
||||
logoPosition: game.logoPosition,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
@@ -15,7 +15,7 @@ export const uploadGamesBatch = async () => {
|
||||
);
|
||||
});
|
||||
|
||||
const gamesChunks = chunk(games, 50);
|
||||
const gamesChunks = chunk(games, 30);
|
||||
|
||||
for (const chunk of gamesChunks) {
|
||||
await HydraApi.post(
|
||||
@@ -26,6 +26,8 @@ export const uploadGamesBatch = async () => {
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
shop: game.shop,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
isFavorite: game.favorite,
|
||||
isPinned: game.pinned ?? false,
|
||||
};
|
||||
})
|
||||
).catch(() => {});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,102 @@
|
||||
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
|
||||
import Piscina from "piscina";
|
||||
|
||||
import { app } from "electron";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import YAML from "yaml";
|
||||
|
||||
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
||||
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
|
||||
import cp from "node:child_process";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class Ludusavi {
|
||||
private static ludusaviPath = path.join(
|
||||
SystemPath.getPath("appData"),
|
||||
private static ludusaviResourcesPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi");
|
||||
|
||||
private static configPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
"ludusavi"
|
||||
);
|
||||
private static ludusaviConfigPath = path.join(
|
||||
this.ludusaviPath,
|
||||
"config.yaml"
|
||||
);
|
||||
private static binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi", "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi", "ludusavi");
|
||||
private static binaryName =
|
||||
process.platform === "win32" ? "ludusavi.exe" : "ludusavi";
|
||||
|
||||
private static worker = new Piscina({
|
||||
filename: ludusaviWorkerPath,
|
||||
workerData: {
|
||||
binaryPath: this.binaryPath,
|
||||
},
|
||||
maxThreads: 1,
|
||||
});
|
||||
|
||||
static async getConfig() {
|
||||
if (!fs.existsSync(this.ludusaviConfigPath)) {
|
||||
await this.worker.run(undefined, { name: "generateConfig" });
|
||||
}
|
||||
private static binaryPath = path.join(this.configPath, this.binaryName);
|
||||
|
||||
public static async getConfig() {
|
||||
const config = YAML.parse(
|
||||
fs.readFileSync(this.ludusaviConfigPath, "utf-8")
|
||||
fs.readFileSync(path.join(this.configPath, "config.yaml"), "utf-8")
|
||||
) as LudusaviConfig;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
backupPath: string,
|
||||
winePrefix?: string | null
|
||||
): Promise<LudusaviBackup> {
|
||||
return this.worker.run(
|
||||
{ title: objectId, backupPath, winePrefix },
|
||||
{ name: "backupGame" }
|
||||
);
|
||||
public static async copyConfigFileToUserData() {
|
||||
if (!fs.existsSync(this.configPath)) {
|
||||
fs.mkdirSync(this.configPath, { recursive: true });
|
||||
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviResourcesPath, "config.yaml"),
|
||||
path.join(this.configPath, "config.yaml")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static async getBackupPreview(
|
||||
public static async copyBinaryToUserData() {
|
||||
if (!fs.existsSync(this.binaryPath)) {
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviResourcesPath, this.binaryName),
|
||||
this.binaryPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
backupPath?: string | null,
|
||||
winePrefix?: string | null,
|
||||
preview?: boolean
|
||||
): Promise<LudusaviBackup> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
"--config",
|
||||
this.configPath,
|
||||
"backup",
|
||||
objectId,
|
||||
"--api",
|
||||
"--force",
|
||||
];
|
||||
|
||||
if (preview) args.push("--preview");
|
||||
if (backupPath) args.push("--path", backupPath);
|
||||
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
||||
|
||||
cp.execFile(
|
||||
this.binaryPath,
|
||||
args,
|
||||
(err: cp.ExecFileException | null, stdout: string) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(stdout) as LudusaviBackup);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getBackupPreview(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix?: string | null
|
||||
): Promise<LudusaviBackup | null> {
|
||||
const config = await this.getConfig();
|
||||
|
||||
const backupData = await this.worker.run(
|
||||
{ title: objectId, winePrefix, preview: true },
|
||||
{ name: "backupGame" }
|
||||
const backupData = await this.backupGame(
|
||||
_shop,
|
||||
objectId,
|
||||
null,
|
||||
winePrefix,
|
||||
true
|
||||
);
|
||||
|
||||
const customGame = config.customGames.find(
|
||||
@@ -77,19 +109,6 @@ export class Ludusavi {
|
||||
};
|
||||
}
|
||||
|
||||
static async restoreBackup(backupPath: string) {
|
||||
return this.worker.run(backupPath, { name: "restoreBackup" });
|
||||
}
|
||||
|
||||
static async addManifestToLudusaviConfig() {
|
||||
const config = await this.getConfig();
|
||||
|
||||
config.manifest.enable = false;
|
||||
config.manifest.secondary = [{ url: LUDUSAVI_MANIFEST_URL, enable: true }];
|
||||
|
||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||
}
|
||||
|
||||
static async addCustomGame(title: string, savePath: string | null) {
|
||||
const config = await this.getConfig();
|
||||
const filteredGames = config.customGames.filter(
|
||||
@@ -105,6 +124,10 @@ export class Ludusavi {
|
||||
}
|
||||
|
||||
config.customGames = filteredGames;
|
||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(this.configPath, "config.yaml"),
|
||||
YAML.stringify(config)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ export const publishExtractionCompleteNotification = async (game: Game) => {
|
||||
};
|
||||
|
||||
export const publishNewAchievementNotification = async (info: {
|
||||
achievements: { displayName: string; iconUrl: string }[];
|
||||
achievements: { title: string; iconUrl: string }[];
|
||||
unlockedAchievementCount: number;
|
||||
totalAchievementCount: number;
|
||||
gameTitle: string;
|
||||
@@ -176,12 +176,12 @@ export const publishNewAchievementNotification = async (info: {
|
||||
gameTitle: info.gameTitle,
|
||||
achievementCount: info.achievements.length,
|
||||
}),
|
||||
body: info.achievements.map((a) => a.displayName).join(", "),
|
||||
body: info.achievements.map((a) => a.title).join(", "),
|
||||
icon: (await downloadImage(info.gameIcon)) ?? icon,
|
||||
}
|
||||
: {
|
||||
title: t("achievement_unlocked", { ns: "achievement" }),
|
||||
body: info.achievements[0].displayName,
|
||||
body: info.achievements[0].title,
|
||||
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import crypto from "node:crypto";
|
||||
|
||||
import { pythonRpcLogger } from "./logger";
|
||||
import { Readable } from "node:stream";
|
||||
import { app, dialog, safeStorage } from "electron";
|
||||
import { app, dialog } from "electron";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
interface GamePayload {
|
||||
@@ -22,12 +22,6 @@ const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||
win32: "hydra-python-rpc.exe",
|
||||
};
|
||||
|
||||
const rustBinaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||
darwin: "hydra-httpdl",
|
||||
linux: "hydra-httpdl",
|
||||
win32: "hydra-httpdl.exe",
|
||||
};
|
||||
|
||||
export class PythonRPC {
|
||||
public static readonly BITTORRENT_PORT = "5881";
|
||||
public static readonly RPC_PORT = "8084";
|
||||
@@ -49,18 +43,13 @@ export class PythonRPC {
|
||||
valueEncoding: "utf8",
|
||||
});
|
||||
|
||||
if (existingPassword)
|
||||
return safeStorage.decryptString(Buffer.from(existingPassword, "hex"));
|
||||
if (existingPassword) return existingPassword;
|
||||
|
||||
const newPassword = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
await db.put(
|
||||
levelKeys.rpcPassword,
|
||||
safeStorage.encryptString(newPassword).toString("hex"),
|
||||
{
|
||||
valueEncoding: "utf8",
|
||||
}
|
||||
);
|
||||
await db.put(levelKeys.rpcPassword, newPassword, {
|
||||
valueEncoding: "utf8",
|
||||
});
|
||||
|
||||
return newPassword;
|
||||
}
|
||||
@@ -77,20 +66,6 @@ export class PythonRPC {
|
||||
rpcPassword,
|
||||
initialDownload ? JSON.stringify(initialDownload) : "",
|
||||
initialSeeding ? JSON.stringify(initialSeeding) : "",
|
||||
app.isPackaged
|
||||
? path.join(
|
||||
process.resourcesPath,
|
||||
rustBinaryNameByPlatform[process.platform]!
|
||||
)
|
||||
: path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"rust_rpc",
|
||||
"target",
|
||||
"debug",
|
||||
rustBinaryNameByPlatform[process.platform]!
|
||||
),
|
||||
];
|
||||
|
||||
if (app.isPackaged) {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import axios from "axios";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { crc32 } from "crc";
|
||||
import WinReg from "winreg";
|
||||
import { parseBuffer, writeBuffer } from "steam-shortcut-editor";
|
||||
|
||||
import type { SteamAppDetails } from "@types";
|
||||
import type { SteamAppDetails, SteamShortcut } from "@types";
|
||||
|
||||
import { logger } from "./logger";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export interface SteamAppDetailsResponse {
|
||||
[key: string]: {
|
||||
@@ -11,6 +17,40 @@ export interface SteamAppDetailsResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export const getSteamLocation = async () => {
|
||||
if (process.platform === "linux") {
|
||||
return path.join(SystemPath.getPath("home"), ".local", "share", "Steam");
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return path.join(
|
||||
SystemPath.getPath("home"),
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Steam"
|
||||
);
|
||||
}
|
||||
|
||||
const regKey = new WinReg({
|
||||
hive: WinReg.HKCU,
|
||||
key: "\\Software\\Valve\\Steam",
|
||||
});
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
regKey.get("SteamPath", (err, value) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
reject(new Error("SteamPath not found in registry"));
|
||||
}
|
||||
|
||||
resolve(value.value);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getSteamAppDetails = async (
|
||||
objectId: string,
|
||||
language: string
|
||||
@@ -40,3 +80,96 @@ export const getSteamAppDetails = async (
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
export const getSteamUsersIds = async () => {
|
||||
const steamLocation = await getSteamLocation().catch(() => null);
|
||||
|
||||
if (!steamLocation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userDataPath = path.join(steamLocation, "userdata");
|
||||
|
||||
if (!fs.existsSync(userDataPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userIds = fs.readdirSync(userDataPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
return userIds
|
||||
.filter((dir) => dir.isDirectory())
|
||||
.map((dir) => Number(dir.name));
|
||||
};
|
||||
|
||||
export const getSteamShortcuts = async (steamUserId: number) => {
|
||||
const shortcutsPath = path.join(
|
||||
await getSteamLocation(),
|
||||
"userdata",
|
||||
steamUserId.toString(),
|
||||
"config",
|
||||
"shortcuts.vdf"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(shortcutsPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const shortcuts = parseBuffer(fs.readFileSync(shortcutsPath));
|
||||
|
||||
return shortcuts.shortcuts as SteamShortcut[];
|
||||
};
|
||||
|
||||
export const generateSteamShortcutAppId = (
|
||||
exePath: string,
|
||||
gameName: string
|
||||
) => {
|
||||
const input = exePath + gameName;
|
||||
const crcValue = crc32(input) >>> 0;
|
||||
const steamAppId = (crcValue | 0x80000000) >>> 0;
|
||||
return steamAppId;
|
||||
};
|
||||
|
||||
export const composeSteamShortcut = (
|
||||
title: string,
|
||||
executablePath: string,
|
||||
iconPath: string | null
|
||||
): SteamShortcut => {
|
||||
return {
|
||||
appid: generateSteamShortcutAppId(executablePath, title),
|
||||
appname: title,
|
||||
Exe: `"${executablePath}"`,
|
||||
StartDir: `"${path.dirname(executablePath)}"`,
|
||||
icon: iconPath ?? "",
|
||||
ShortcutPath: "",
|
||||
LaunchOptions: "",
|
||||
IsHidden: false,
|
||||
AllowDesktopConfig: true,
|
||||
AllowOverlay: true,
|
||||
OpenVR: false,
|
||||
Devkit: false,
|
||||
DevkitGameID: "",
|
||||
DevkitOverrideAppID: false,
|
||||
LastPlayTime: 0,
|
||||
FlatpakAppID: "",
|
||||
};
|
||||
};
|
||||
|
||||
export const writeSteamShortcuts = async (
|
||||
steamUserId: number,
|
||||
shortcuts: SteamShortcut[]
|
||||
) => {
|
||||
const buffer = writeBuffer({ shortcuts });
|
||||
|
||||
return fs.promises.writeFile(
|
||||
path.join(
|
||||
await getSteamLocation(),
|
||||
"userdata",
|
||||
steamUserId.toString(),
|
||||
"config",
|
||||
"shortcuts.vdf"
|
||||
),
|
||||
buffer
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export class SystemPath {
|
||||
try {
|
||||
return app.getPath(pathName);
|
||||
} catch (error) {
|
||||
logger.error(`Error getting path: ${error}`);
|
||||
console.error(`Error getting path: ${error}`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Tray,
|
||||
app,
|
||||
nativeImage,
|
||||
screen,
|
||||
shell,
|
||||
} from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
@@ -17,12 +18,17 @@ import { HydraApi } from "./hydra-api";
|
||||
import UserAgent from "user-agents";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { orderBy, slice } from "lodash-es";
|
||||
import type { ScreenState, UserPreferences } from "@types";
|
||||
import { AuthPage } from "@shared";
|
||||
import type {
|
||||
AchievementCustomNotificationPosition,
|
||||
ScreenState,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
|
||||
import { isStaging } from "@main/constants";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
public static notificationWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||
|
||||
@@ -175,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,
|
||||
{
|
||||
@@ -182,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,
|
||||
@@ -201,9 +212,6 @@ export class WindowManager {
|
||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
|
||||
this.mainWindow.webContents.setWindowOpenHandler((handler) => {
|
||||
@@ -259,6 +267,153 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static loadNotificationWindowURL() {
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.notificationWindow?.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
|
||||
);
|
||||
} else {
|
||||
this.notificationWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash: "achievement-notification",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly NOTIFICATION_WINDOW_WIDTH = 360;
|
||||
private static readonly NOTIFICATION_WINDOW_HEIGHT = 140;
|
||||
|
||||
private static async getNotificationWindowPosition(
|
||||
position: AchievementCustomNotificationPosition | undefined
|
||||
) {
|
||||
const display = screen.getPrimaryDisplay();
|
||||
const { width, height } = display.workAreaSize;
|
||||
|
||||
if (position === "bottom-left") {
|
||||
return {
|
||||
x: 0,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "bottom-center") {
|
||||
return {
|
||||
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "bottom-right") {
|
||||
return {
|
||||
x: width - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "top-center") {
|
||||
return {
|
||||
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "top-right") {
|
||||
return {
|
||||
x: width - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public static async createNotificationWindow() {
|
||||
if (this.notificationWindow) return;
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | undefined>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
userPreferences?.achievementNotificationsEnabled === false ||
|
||||
userPreferences?.achievementCustomNotificationsEnabled === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y } = await this.getNotificationWindowPosition(
|
||||
userPreferences?.achievementCustomNotificationPosition
|
||||
);
|
||||
|
||||
this.notificationWindow = new BrowserWindow({
|
||||
transparent: true,
|
||||
maximizable: false,
|
||||
autoHideMenuBar: true,
|
||||
minimizable: false,
|
||||
backgroundColor: "#00000000",
|
||||
focusable: false,
|
||||
skipTaskbar: true,
|
||||
frame: false,
|
||||
width: this.NOTIFICATION_WINDOW_WIDTH,
|
||||
height: this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
x,
|
||||
y,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||
|
||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
this.loadNotificationWindowURL();
|
||||
|
||||
if (!app.isPackaged || isStaging) {
|
||||
this.notificationWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
public static async showAchievementTestNotification() {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
const language = userPreferences.language ?? "en";
|
||||
|
||||
this.notificationWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
userPreferences.achievementCustomNotificationPosition ?? "top-left",
|
||||
[
|
||||
generateAchievementCustomNotificationTest(t, language),
|
||||
generateAchievementCustomNotificationTest(t, language, {
|
||||
isRare: true,
|
||||
isHidden: true,
|
||||
}),
|
||||
generateAchievementCustomNotificationTest(t, language, {
|
||||
isPlatinum: true,
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static async closeNotificationWindow() {
|
||||
if (this.notificationWindow) {
|
||||
this.notificationWindow.close();
|
||||
this.notificationWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static openEditorWindow(themeId: string) {
|
||||
if (this.mainWindow) {
|
||||
const existingWindow = this.editorWindows.get(themeId);
|
||||
@@ -271,13 +426,13 @@ export class WindowManager {
|
||||
}
|
||||
|
||||
const editorWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
width: 720,
|
||||
height: 720,
|
||||
minWidth: 600,
|
||||
minHeight: 540,
|
||||
backgroundColor: "#1c1c1c",
|
||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
icon,
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
titleBarOverlay: {
|
||||
symbolColor: "#DADBE1",
|
||||
@@ -308,14 +463,13 @@ export class WindowManager {
|
||||
editorWindow.once("ready-to-show", () => {
|
||||
editorWindow.show();
|
||||
this.mainWindow?.webContents.openDevTools();
|
||||
if (isStaging) {
|
||||
if (!app.isPackaged || isStaging) {
|
||||
editorWindow.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
editorWindow.webContents.on("before-input-event", (event, input) => {
|
||||
editorWindow.webContents.on("before-input-event", (_event, input) => {
|
||||
if (input.key === "F12") {
|
||||
event.preventDefault();
|
||||
this.mainWindow?.webContents.toggleDevTools();
|
||||
}
|
||||
});
|
||||
@@ -430,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", () => {
|
||||
@@ -443,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);
|
||||
|
||||
30
src/main/services/wine.ts
Normal file
30
src/main/services/wine.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export class Wine {
|
||||
public static validatePrefix(winePrefixPath: string) {
|
||||
const requiredFiles = [
|
||||
{ name: "system.reg", type: "file" },
|
||||
{ name: "user.reg", type: "file" },
|
||||
{ name: "userdef.reg", type: "file" },
|
||||
{ name: "dosdevices", type: "dir" },
|
||||
{ name: "drive_c", type: "dir" },
|
||||
];
|
||||
|
||||
for (const file of requiredFiles) {
|
||||
const filePath = path.join(winePrefixPath, file.name);
|
||||
|
||||
if (file.type === "file" && !fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.type === "dir") {
|
||||
if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
import type { FriendGameSession } from "@main/generated/envelope";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services/hydra-api";
|
||||
import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications";
|
||||
import { GameStats } from "@types";
|
||||
import type { GameStats, UserPreferences, UserProfile } from "@types";
|
||||
|
||||
export const friendGameSessionEvent = async (payload: FriendGameSession) => {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences?.friendStartGameNotificationsEnabled === false) return;
|
||||
|
||||
const [friend, gameStats] = await Promise.all([
|
||||
HydraApi.get(`/users/${payload.friendId}`),
|
||||
HydraApi.get<UserProfile>(`/users/${payload.friendId}`),
|
||||
HydraApi.get<GameStats>(
|
||||
`/games/stats?objectId=${payload.objectId}&shop=steam`
|
||||
),
|
||||
]);
|
||||
]).catch(() => [null, null]);
|
||||
|
||||
if (friend && gameStats) {
|
||||
publishFriendStartedPlayingGameNotification(friend, gameStats);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { LudusaviBackup } from "@types";
|
||||
import cp from "node:child_process";
|
||||
|
||||
import { workerData } from "node:worker_threads";
|
||||
|
||||
const { binaryPath } = workerData;
|
||||
|
||||
export const backupGame = ({
|
||||
title,
|
||||
backupPath,
|
||||
preview = false,
|
||||
winePrefix,
|
||||
}: {
|
||||
title: string;
|
||||
backupPath: string;
|
||||
preview?: boolean;
|
||||
winePrefix?: string;
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = ["backup", title, "--api", "--force"];
|
||||
|
||||
if (preview) args.push("--preview");
|
||||
if (backupPath) args.push("--path", backupPath);
|
||||
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
||||
|
||||
cp.execFile(
|
||||
binaryPath,
|
||||
args,
|
||||
(err: cp.ExecFileException | null, stdout: string) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(stdout) as LudusaviBackup);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const restoreBackup = (backupPath: string) => {
|
||||
const result = cp.execFileSync(binaryPath, [
|
||||
"restore",
|
||||
"--path",
|
||||
backupPath,
|
||||
"--api",
|
||||
"--force",
|
||||
]);
|
||||
|
||||
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||
};
|
||||
|
||||
export const generateConfig = () => {
|
||||
const result = cp.execFileSync(binaryPath, ["schema", "config"]);
|
||||
|
||||
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||
};
|
||||
@@ -18,6 +18,8 @@ import type {
|
||||
FriendRequestSync,
|
||||
ShortcutLocation,
|
||||
ShopAssets,
|
||||
AchievementCustomNotificationPosition,
|
||||
AchievementNotificationInfo,
|
||||
} from "@types";
|
||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
@@ -141,6 +143,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||
addGameToPinned: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("addGameToPinned", shop, objectId),
|
||||
removeGameFromPinned: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("removeGameFromPinned", shop, objectId),
|
||||
updateLaunchOptions: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -187,8 +193,14 @@ 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: () =>
|
||||
ipcRenderer.invoke("getDefaultWinePrefixSelectionPath"),
|
||||
createSteamShortcut: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("createSteamShortcut", shop, objectId),
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
@@ -205,12 +217,6 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
||||
},
|
||||
onAchievementUnlocked: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-achievement-unlocked", listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
||||
},
|
||||
onExtractionComplete: (cb: (shop: GameShop, objectId: string) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
@@ -234,6 +240,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 +370,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* User */
|
||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||
getUserLibrary: (userId: string, take?: number, skip?: number) =>
|
||||
ipcRenderer.invoke("getUserLibrary", userId, take, skip),
|
||||
blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
|
||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||
@@ -408,6 +420,42 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||
onAchievementUnlocked: (
|
||||
cb: (
|
||||
position?: AchievementCustomNotificationPosition,
|
||||
achievements?: AchievementNotificationInfo[]
|
||||
) => void
|
||||
) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
position?: AchievementCustomNotificationPosition,
|
||||
achievements?: AchievementNotificationInfo[]
|
||||
) => cb(position, achievements);
|
||||
ipcRenderer.on("on-achievement-unlocked", listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
||||
},
|
||||
onCombinedAchievementsUnlocked: (
|
||||
cb: (
|
||||
gameCount: number,
|
||||
achievementsCount: number,
|
||||
position: AchievementCustomNotificationPosition
|
||||
) => void
|
||||
) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
gameCount: number,
|
||||
achievementCount: number,
|
||||
position: AchievementCustomNotificationPosition
|
||||
) => cb(gameCount, achievementCount, position);
|
||||
ipcRenderer.on("on-combined-achievements-unlocked", listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-combined-achievements-unlocked", listener);
|
||||
},
|
||||
updateAchievementCustomNotificationWindow: () =>
|
||||
ipcRenderer.invoke("updateAchievementCustomNotificationWindow"),
|
||||
showAchievementTestNotification: () =>
|
||||
ipcRenderer.invoke("showAchievementTestNotification"),
|
||||
|
||||
/* Themes */
|
||||
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),
|
||||
@@ -426,11 +474,11 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
/* Editor */
|
||||
openEditorWindow: (themeId: string) =>
|
||||
ipcRenderer.invoke("openEditorWindow", themeId),
|
||||
onCssInjected: (cb: (cssString: string) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, cssString: string) =>
|
||||
cb(cssString);
|
||||
ipcRenderer.on("css-injected", listener);
|
||||
return () => ipcRenderer.removeListener("css-injected", listener);
|
||||
onCustomThemeUpdated: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-custom-theme-updated", listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-custom-theme-updated", listener);
|
||||
},
|
||||
closeEditorWindow: (themeId?: string) =>
|
||||
ipcRenderer.invoke("closeEditorWindow", themeId),
|
||||
|
||||
@@ -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:;"
|
||||
|
||||
@@ -28,7 +28,7 @@ import { downloadSourcesTable } from "./dexie";
|
||||
import { useSubscription } from "./hooks/use-subscription";
|
||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||
|
||||
import { injectCustomCss } from "./helpers";
|
||||
import { injectCustomCss, removeCustomCss } from "./helpers";
|
||||
import "./app.scss";
|
||||
|
||||
export interface AppProps {
|
||||
@@ -246,17 +246,27 @@ export function App() {
|
||||
};
|
||||
}, [updateRepacks]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAndApplyTheme = async () => {
|
||||
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||
|
||||
if (activeTheme?.code) {
|
||||
injectCustomCss(activeTheme.code);
|
||||
}
|
||||
};
|
||||
loadAndApplyTheme();
|
||||
const loadAndApplyTheme = useCallback(async () => {
|
||||
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||
if (activeTheme?.code) {
|
||||
injectCustomCss(activeTheme.code);
|
||||
} else {
|
||||
removeCustomCss();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAndApplyTheme();
|
||||
}, [loadAndApplyTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onCustomThemeUpdated(() => {
|
||||
loadAndApplyTheme();
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [loadAndApplyTheme]);
|
||||
|
||||
const playAudio = useCallback(() => {
|
||||
const audio = new Audio(achievementSound);
|
||||
audio.volume = 0.2;
|
||||
@@ -273,14 +283,6 @@ export function App() {
|
||||
};
|
||||
}, [playAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
||||
injectCustomCss(cssString);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
dispatch(closeToast());
|
||||
}, [dispatch]);
|
||||
|
||||
BIN
src/renderer/src/assets/icons/ellipses.png
Normal file
BIN
src/renderer/src/assets/icons/ellipses.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
5
src/renderer/src/assets/icons/trophy.svg
Normal file
5
src/renderer/src/assets/icons/trophy.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Frame 933">
|
||||
<path id="Vector" d="M29.3333 10.5H26.8333V8.83333C26.8333 8.61232 26.7455 8.40036 26.5893 8.24408C26.433 8.0878 26.221 8 26 8H11C10.779 8 10.567 8.0878 10.4107 8.24408C10.2545 8.40036 10.1667 8.61232 10.1667 8.83333V10.5H7.66667C7.22464 10.5 6.80072 10.6756 6.48816 10.9882C6.17559 11.3007 6 11.7246 6 12.1667V13.8333C6 14.9384 6.43899 15.9982 7.22039 16.7796C7.6073 17.1665 8.06663 17.4734 8.57215 17.6828C9.07768 17.8922 9.61949 18 10.1667 18H10.5469C11.0378 19.5556 11.9737 20.9333 13.2391 21.9628C14.5044 22.9923 16.0437 23.6285 17.6667 23.7927V26.3333H15.1667C14.9457 26.3333 14.7337 26.4211 14.5774 26.5774C14.4211 26.7337 14.3333 26.9457 14.3333 27.1667C14.3333 27.3877 14.4211 27.5996 14.5774 27.7559C14.7337 27.9122 14.9457 28 15.1667 28H21.8333C22.0543 28 22.2663 27.9122 22.4226 27.7559C22.5789 27.5996 22.6667 27.3877 22.6667 27.1667C22.6667 26.9457 22.5789 26.7337 22.4226 26.5774C22.2663 26.4211 22.0543 26.3333 21.8333 26.3333H19.3333V23.7896C22.6604 23.4531 25.4208 21.1187 26.425 18H26.8333C27.9384 18 28.9982 17.561 29.7796 16.7796C30.561 15.9982 31 14.9384 31 13.8333V12.1667C31 11.7246 30.8244 11.3007 30.5118 10.9882C30.1993 10.6756 29.7754 10.5 29.3333 10.5ZM10.1667 16.3333C9.50363 16.3333 8.86774 16.0699 8.3989 15.6011C7.93006 15.1323 7.66667 14.4964 7.66667 13.8333V12.1667H10.1667V15.5C10.1667 15.7778 10.1802 16.0556 10.2073 16.3333H10.1667ZM29.3333 13.8333C29.3333 14.4964 29.0699 15.1323 28.6011 15.6011C28.1323 16.0699 27.4964 16.3333 26.8333 16.3333H26.7812C26.8154 16.0255 26.8328 15.716 26.8333 15.4062V12.1667H29.3333V13.8333Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,519 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
$margin-horizontal: 40px;
|
||||
$margin-top: 52px;
|
||||
$margin-bottom: 28px;
|
||||
|
||||
@keyframes content-in {
|
||||
0% {
|
||||
width: 80px;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
width: 80px;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes content-wait {
|
||||
0% {
|
||||
width: 80px;
|
||||
}
|
||||
100% {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes trophy-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ellipses-stand-by {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes ellipses-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
scale: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes content-expand {
|
||||
0% {
|
||||
width: 80px;
|
||||
}
|
||||
100% {
|
||||
width: calc(360px - $margin-horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes chip-stand-by {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes chip-in {
|
||||
0% {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes title-in {
|
||||
0% {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes description-in {
|
||||
0% {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dark-overlay {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes content-out {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
from {
|
||||
transform: translateX(0px) rotate(36deg);
|
||||
}
|
||||
to {
|
||||
transform: translateX(420px) rotate(36deg);
|
||||
}
|
||||
}
|
||||
|
||||
.achievement-notification {
|
||||
width: 360px;
|
||||
height: 140px;
|
||||
display: flex;
|
||||
|
||||
&--top-left {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&--top-center {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&--top-right {
|
||||
justify-content: end;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&--bottom-left {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
&--bottom-center {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
&--bottom-right {
|
||||
justify-content: end;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
&__outer-container {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: calc(360px - $margin-horizontal);
|
||||
overflow: clip;
|
||||
border: 1px solid #ffffff1a;
|
||||
animation:
|
||||
content-in 450ms ease-in-out,
|
||||
content-wait 450ms ease-in-out 450ms,
|
||||
content-expand 450ms ease-in-out 900ms;
|
||||
box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&--top-left &__outer-container {
|
||||
margin: $margin-top 0 0 $margin-horizontal;
|
||||
}
|
||||
|
||||
&--top-center &__outer-container {
|
||||
margin: $margin-top 0 0 $margin-horizontal;
|
||||
}
|
||||
|
||||
&--top-right &__outer-container {
|
||||
margin: $margin-top $margin-horizontal 0 0;
|
||||
}
|
||||
|
||||
&--bottom-left &__outer-container {
|
||||
margin: 0 0 $margin-bottom $margin-horizontal;
|
||||
}
|
||||
|
||||
&--bottom-center &__outer-container {
|
||||
margin: 0 0 $margin-bottom $margin-horizontal;
|
||||
}
|
||||
|
||||
&--bottom-right &__outer-container {
|
||||
margin: 0 $margin-horizontal $margin-bottom 0;
|
||||
}
|
||||
|
||||
&--closing .achievement-notification__outer-container {
|
||||
animation: content-out 450ms ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&__container {
|
||||
width: calc(360px - $margin-horizontal);
|
||||
display: flex;
|
||||
padding: 8px 16px 8px 8px;
|
||||
background: globals.$background-color;
|
||||
}
|
||||
|
||||
&--platinum &__container {
|
||||
background: linear-gradient(94deg, #1c1c1c -25%, #044838 100%);
|
||||
}
|
||||
|
||||
&--rare &__container {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -60px;
|
||||
width: 29px;
|
||||
height: 134px;
|
||||
transform: translateX(0px) rotate(36deg);
|
||||
opacity: 0.2;
|
||||
background: #d9d9d9;
|
||||
filter: blur(8px);
|
||||
animation: shine 450ms ease-in-out 1350ms;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
box-sizing: border-box;
|
||||
min-width: 64px;
|
||||
min-height: 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&--rare &__icon {
|
||||
outline: 1px solid #f4a510;
|
||||
box-shadow: 0px 0px 12px 0px rgba(244, 165, 16, 0.25);
|
||||
}
|
||||
|
||||
&--platinum &__icon {
|
||||
outline: 1px solid #0cf1ca;
|
||||
box-shadow: 0px 0px 12px 0px rgba(12, 241, 202, 0.25);
|
||||
}
|
||||
|
||||
&__additional-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&__dark-overlay {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
animation: dark-overlay 900ms ease-in-out;
|
||||
}
|
||||
|
||||
&__trophy-overlay {
|
||||
position: absolute;
|
||||
mask-image: url("/src/assets/icons/trophy.svg");
|
||||
top: 22px;
|
||||
left: 22px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
animation: trophy-out 900ms ease-in-out;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&--rare &__trophy-overlay {
|
||||
background: linear-gradient(
|
||||
118deg,
|
||||
#e8ad15 18.96%,
|
||||
#d5900f 26.41%,
|
||||
#e8ad15 29.99%,
|
||||
#e4aa15 38.89%,
|
||||
#ca890e 42.43%,
|
||||
#ca880e 46.59%,
|
||||
#ecbe1a 50.08%,
|
||||
#ecbd1a 53.48%,
|
||||
#b3790d 57.39%,
|
||||
#66470a 75.64%,
|
||||
#a37a13 78.2%,
|
||||
#987112 79.28%,
|
||||
#503808 83.6%,
|
||||
#3e2d08 85.77%
|
||||
),
|
||||
#fff;
|
||||
}
|
||||
|
||||
&--platinum &__trophy-overlay {
|
||||
background: linear-gradient(
|
||||
118deg,
|
||||
#15e8d6 18.96%,
|
||||
#0fd5a7 26.41%,
|
||||
#15e8b7 29.99%,
|
||||
#15e4b4 38.89%,
|
||||
#0eca7f 42.43%,
|
||||
#0eca9e 46.59%,
|
||||
#1aecbb 50.08%,
|
||||
#1aecb0 53.48%,
|
||||
#0db392 57.39%,
|
||||
#0a6648 75.64%,
|
||||
#13a38b 78.2%,
|
||||
#129862 79.28%,
|
||||
#085042 83.6%,
|
||||
#083e31 85.77%
|
||||
);
|
||||
}
|
||||
|
||||
&__ellipses-overlay {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
animation: ellipses-out 900ms ease-in-out;
|
||||
}
|
||||
|
||||
&__text-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: globals.$muted-color;
|
||||
animation: title-in 450ms ease-in-out 900ms;
|
||||
}
|
||||
|
||||
&__hidden-icon {
|
||||
margin-right: 4px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 2; /* number of lines to show */
|
||||
line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
color: globals.$body-color;
|
||||
animation: description-in 450ms ease-in-out 900ms;
|
||||
}
|
||||
|
||||
&--closing &__chip {
|
||||
animation: content-out 450ms ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&__chip {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
border-radius: 300px;
|
||||
align-items: center;
|
||||
background: globals.$muted-color;
|
||||
height: 24px;
|
||||
animation:
|
||||
chip-stand-by 900ms ease-in-out,
|
||||
chip-in 450ms ease-in-out 900ms;
|
||||
z-index: 2;
|
||||
|
||||
&__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
path {
|
||||
fill: globals.$background-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: globals.$background-color;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&--top-left &__chip {
|
||||
top: -12px;
|
||||
margin: $margin-top 0 0 $margin-horizontal;
|
||||
}
|
||||
|
||||
&--top-center &__chip {
|
||||
top: -12px;
|
||||
margin: $margin-top 0 0 $margin-horizontal;
|
||||
}
|
||||
|
||||
&--top-right &__chip {
|
||||
top: -12px;
|
||||
margin: $margin-top $margin-horizontal 0 0;
|
||||
}
|
||||
|
||||
&--bottom-left &__chip {
|
||||
bottom: 70px;
|
||||
margin: 0 0 $margin-bottom $margin-horizontal;
|
||||
}
|
||||
|
||||
&--bottom-center &__chip {
|
||||
bottom: 70px;
|
||||
margin: 0 0 $margin-bottom $margin-horizontal;
|
||||
}
|
||||
|
||||
&--bottom-right &__chip {
|
||||
bottom: 70px;
|
||||
margin: 0 $margin-horizontal $margin-bottom 0;
|
||||
}
|
||||
|
||||
&--rare &__chip {
|
||||
background: linear-gradient(
|
||||
160deg,
|
||||
#e8ad15 18.96%,
|
||||
#d5900f 26.41%,
|
||||
#e8ad15 29.99%,
|
||||
#e4aa15 38.89%,
|
||||
#ca890e 42.43%,
|
||||
#ca880e 46.59%,
|
||||
#ecbe1a 50.08%,
|
||||
#ecbd1a 53.48%,
|
||||
#b3790d 57.39%,
|
||||
#66470a 75.64%,
|
||||
#a37a13 78.2%,
|
||||
#987112 79.28%,
|
||||
#503808 83.6%,
|
||||
#3e2d08 85.77%
|
||||
);
|
||||
&__icon {
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
&__label {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&--platinum &__chip {
|
||||
background: linear-gradient(
|
||||
118deg,
|
||||
#15e8d6 18.96%,
|
||||
#0fd5a7 26.41%,
|
||||
#15e8b7 29.99%,
|
||||
#15e4b4 38.89%,
|
||||
#0eca7f 42.43%,
|
||||
#0eca9e 46.59%,
|
||||
#1aecbb 50.08%,
|
||||
#1aecb0 53.48%,
|
||||
#0db392 57.39%,
|
||||
#0a6648 75.64%,
|
||||
#13a38b 78.2%,
|
||||
#129862 79.28%,
|
||||
#085042 83.6%,
|
||||
#083e31 85.77%
|
||||
);
|
||||
&__icon {
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
&__label {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&--closing * {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&--closing *::before,
|
||||
&--closing *::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
AchievementCustomNotificationPosition,
|
||||
AchievementNotificationInfo,
|
||||
} from "@types";
|
||||
import cn from "classnames";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { EyeClosedIcon } from "@primer/octicons-react";
|
||||
import Ellipses from "@renderer/assets/icons/ellipses.png";
|
||||
import "./achievement-notification.scss";
|
||||
|
||||
interface AchievementNotificationProps {
|
||||
position: AchievementCustomNotificationPosition;
|
||||
achievement: AchievementNotificationInfo;
|
||||
isClosing: boolean;
|
||||
}
|
||||
|
||||
export function AchievementNotificationItem({
|
||||
position,
|
||||
achievement,
|
||||
isClosing,
|
||||
}: Readonly<AchievementNotificationProps>) {
|
||||
const baseClassName = "achievement-notification";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("achievement-notification", {
|
||||
[`${baseClassName}--${position}`]: true,
|
||||
[`${baseClassName}--closing`]: isClosing,
|
||||
[`${baseClassName}--hidden`]: achievement.isHidden,
|
||||
[`${baseClassName}--rare`]: achievement.isRare,
|
||||
[`${baseClassName}--platinum`]: achievement.isPlatinum,
|
||||
})}
|
||||
>
|
||||
{achievement.points !== undefined && (
|
||||
<div className="achievement-notification__chip">
|
||||
<HydraIcon className="achievement-notification__chip__icon" />
|
||||
<span className="achievement-notification__chip__label">
|
||||
+{achievement.points}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="achievement-notification__outer-container">
|
||||
<div className="achievement-notification__container">
|
||||
<div className="achievement-notification__content">
|
||||
<img
|
||||
src={achievement.iconUrl}
|
||||
alt={achievement.title}
|
||||
className="achievement-notification__icon"
|
||||
/>
|
||||
<div className="achievement-notification__text-container">
|
||||
<p className="achievement-notification__title">
|
||||
{achievement.isHidden && (
|
||||
<span className="achievement-notification__hidden-icon">
|
||||
<EyeClosedIcon size={16} />
|
||||
</span>
|
||||
)}
|
||||
{achievement.title}
|
||||
</p>
|
||||
<p className="achievement-notification__description">
|
||||
{achievement.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="achievement-notification__additional-overlay">
|
||||
<div className="achievement-notification__dark-overlay"></div>
|
||||
<img
|
||||
className="achievement-notification__ellipses-overlay"
|
||||
src={Ellipses}
|
||||
alt="Ellipses effect"
|
||||
/>
|
||||
<div className="achievement-notification__trophy-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import cn from "classnames";
|
||||
import { PlacesType, Tooltip } from "react-tooltip";
|
||||
|
||||
import "./button.scss";
|
||||
import { useId } from "react";
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
> {
|
||||
tooltip?: string;
|
||||
tooltipPlace?: PlacesType;
|
||||
theme?: "primary" | "outline" | "dark" | "danger";
|
||||
}
|
||||
|
||||
@@ -14,15 +18,32 @@ export function Button({
|
||||
children,
|
||||
theme = "primary",
|
||||
className,
|
||||
tooltip,
|
||||
tooltipPlace = "top",
|
||||
...props
|
||||
}: Readonly<ButtonProps>) {
|
||||
const id = useId();
|
||||
|
||||
const tooltipProps = tooltip
|
||||
? {
|
||||
"data-tooltip-id": id,
|
||||
"data-tooltip-place": tooltipPlace,
|
||||
"data-tooltip-content": tooltip,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn("button", `button--${theme}`, className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("button", `button--${theme}`, className)}
|
||||
{...props}
|
||||
{...tooltipProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
{tooltip && <Tooltip id={id} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.collapsed-menu {
|
||||
&__button {
|
||||
height: 72px;
|
||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: globals.$background-color;
|
||||
color: globals.$muted-color;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.2s;
|
||||
gap: globals.$spacing-unit;
|
||||
font-size: globals.$body-font-size;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: globals.$active-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
transition: transform ease 0.2s;
|
||||
|
||||
&--open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.4s cubic-bezier(0, 1, 0, 1);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ChevronDownIcon } from "@primer/octicons-react";
|
||||
import "./collapsed-menu.scss";
|
||||
|
||||
export interface CollapsedMenuProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CollapsedMenu({
|
||||
title,
|
||||
children,
|
||||
}: Readonly<CollapsedMenuProps>) {
|
||||
const content = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (content.current && content.current.scrollHeight !== height) {
|
||||
setHeight(isOpen ? content.current.scrollHeight : 0);
|
||||
} else if (!isOpen) {
|
||||
setHeight(0);
|
||||
}
|
||||
}, [isOpen, children, height]);
|
||||
|
||||
return (
|
||||
<div className="collapsed-menu">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="collapsed-menu__button"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={`collapsed-menu__chevron ${
|
||||
isOpen ? "collapsed-menu__chevron--open" : ""
|
||||
}`}
|
||||
/>
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
ref={content}
|
||||
className="collapsed-menu__content"
|
||||
style={{
|
||||
maxHeight: `${height}px`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,12 +18,13 @@ export function SelectField({
|
||||
options = [{ key: "-", value: value?.toString() || "-", label: "-" }],
|
||||
theme = "primary",
|
||||
onChange,
|
||||
}: SelectProps) {
|
||||
className,
|
||||
}: Readonly<SelectProps>) {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className="select-field__container">
|
||||
<div className={cn("select-field__container", className)}>
|
||||
{label && (
|
||||
<label htmlFor={id} className="select-field__label">
|
||||
{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,24 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ 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 } from "@primer/octicons-react";
|
||||
import { SidebarGameItem } from "./sidebar-game-item";
|
||||
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
||||
import { useDispatch } from "react-redux";
|
||||
@@ -32,6 +32,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 +62,12 @@ export function Sidebar() {
|
||||
|
||||
const { showWarningToast } = useToast();
|
||||
|
||||
const [showPlayableOnly, setShowPlayableOnly] = useState(false);
|
||||
|
||||
const handlePlayButtonClick = () => {
|
||||
setShowPlayableOnly(!showPlayableOnly);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateLibrary();
|
||||
}, [lastPacket?.gameId, updateLibrary]);
|
||||
@@ -242,7 +250,20 @@ 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>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("sidebar__play-button", {
|
||||
"sidebar__play-button--active": showPlayableOnly,
|
||||
})}
|
||||
onClick={handlePlayButtonClick}
|
||||
>
|
||||
<PlayIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
ref={filterRef}
|
||||
@@ -254,6 +275,7 @@ export function Sidebar() {
|
||||
<ul className="sidebar__menu">
|
||||
{filteredLibrary
|
||||
.filter((game) => !game.favorite)
|
||||
.filter((game) => !showPlayableOnly || isGamePlayable(game))
|
||||
.map((game) => (
|
||||
<SidebarGameItem
|
||||
key={game.id}
|
||||
|
||||
@@ -74,6 +74,6 @@
|
||||
}
|
||||
|
||||
&__error-label {
|
||||
color: globals.$danger-color;
|
||||
color: globals.$error-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export const VERSION_CODENAME = "Polychrome";
|
||||
export const VERSION_CODENAME = "Lumen";
|
||||
|
||||
export const DOWNLOADER_NAME = {
|
||||
[Downloader.RealDebrid]: "Real-Debrid",
|
||||
|
||||
@@ -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}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user