mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 16:53:57 +00:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6c55d667bd | ||
|
|
a4bdc3b5f0 | ||
|
|
44dc8f73e8 | ||
|
|
8b8ead531d | ||
|
|
ec40dfdb0b | ||
|
|
74d93da9b3 | ||
|
|
216f813771 | ||
|
|
fee9cfb3e8 | ||
|
|
dcd00cda98 | ||
|
|
c9135715fa | ||
|
|
64cea7ff85 | ||
|
|
382a618c3f | ||
|
|
d906e3f145 | ||
|
|
e987b27aec | ||
|
|
18815a027f | ||
|
|
6c44cc0cc4 | ||
|
|
171c728616 | ||
|
|
b4ff16cfa4 | ||
|
|
eb6317e659 | ||
|
|
8377f85f0b | ||
|
|
10504cdaf8 | ||
|
|
a01c77b424 | ||
|
|
408adb566c | ||
|
|
cf818d0f4f | ||
|
|
48e9536169 | ||
|
|
00c589a138 | ||
|
|
30584492af | ||
|
|
aa18b57ada |
@@ -1,4 +1,5 @@
|
|||||||
MAIN_VITE_API_URL=API_URL
|
MAIN_VITE_API_URL=API_URL
|
||||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||||
|
MAIN_VITE_WS_URL=
|
||||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
|
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
|
||||||
RENDERER_VITE_TORBOX_REFERRAL_CODE=
|
RENDERER_VITE_TORBOX_REFERRAL_CODE=
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ out
|
|||||||
.gitignore
|
.gitignore
|
||||||
migration.stub
|
migration.stub
|
||||||
hydra-python-rpc/
|
hydra-python-rpc/
|
||||||
|
src/main/generated/
|
||||||
|
|||||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -48,6 +48,7 @@ jobs:
|
|||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||||
|
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
|
||||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -63,6 +64,7 @@ jobs:
|
|||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||||
|
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
|
||||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -97,3 +99,4 @@ jobs:
|
|||||||
dist/*.yml
|
dist/*.yml
|
||||||
dist/*.blockmap
|
dist/*.blockmap
|
||||||
dist/*.pacman
|
dist/*.pacman
|
||||||
|
dist/*.AppImage
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -51,6 +51,7 @@ jobs:
|
|||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
|
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||||
@@ -66,6 +67,7 @@ jobs:
|
|||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
|
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,7 +7,8 @@ out
|
|||||||
*.log*
|
*.log*
|
||||||
.env
|
.env
|
||||||
.vite
|
.vite
|
||||||
ludusavi/
|
ludusavi/**
|
||||||
|
!ludusavi/config.yaml
|
||||||
hydra-python-rpc/
|
hydra-python-rpc/
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "proto"]
|
||||||
|
path = proto
|
||||||
|
url = https://github.com/hydralauncher/hydra-protos.git
|
||||||
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:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
extraResources:
|
extraResources:
|
||||||
- aria2
|
|
||||||
- ludusavi
|
- ludusavi
|
||||||
- hydra-python-rpc
|
- hydra-python-rpc
|
||||||
- seeds
|
- seeds
|
||||||
@@ -21,6 +20,7 @@ asarUnpack:
|
|||||||
win:
|
win:
|
||||||
executableName: Hydra
|
executableName: Hydra
|
||||||
extraResources:
|
extraResources:
|
||||||
|
- from: binaries/aria2c.exe
|
||||||
- from: binaries/7z.exe
|
- from: binaries/7z.exe
|
||||||
- from: binaries/7z.dll
|
- from: binaries/7z.dll
|
||||||
target:
|
target:
|
||||||
@@ -51,6 +51,7 @@ dmg:
|
|||||||
linux:
|
linux:
|
||||||
extraResources:
|
extraResources:
|
||||||
- from: binaries/7zzs
|
- from: binaries/7zzs
|
||||||
|
- from: binaries/aria2c
|
||||||
target:
|
target:
|
||||||
- AppImage
|
- AppImage
|
||||||
- snap
|
- snap
|
||||||
|
|||||||
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: []
|
||||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.4.8",
|
"version": "3.5.2",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
@@ -28,7 +28,8 @@
|
|||||||
"build:win": "electron-vite build && electron-builder --win",
|
"build:win": "electron-vite build && electron-builder --win",
|
||||||
"build:mac": "electron-vite build && electron-builder --mac",
|
"build:mac": "electron-vite build && electron-builder --mac",
|
||||||
"build:linux": "electron-vite build && electron-builder --linux",
|
"build:linux": "electron-vite build && electron-builder --linux",
|
||||||
"prepare": "husky"
|
"prepare": "husky",
|
||||||
|
"protoc": "npx protoc --ts_out src/main/generated --proto_path proto proto/*.proto"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
|
"crc": "^4.3.2",
|
||||||
"create-desktop-shortcuts": "^1.11.1",
|
"create-desktop-shortcuts": "^1.11.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dexie": "^4.0.10",
|
"dexie": "^4.0.10",
|
||||||
@@ -61,19 +63,22 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"parse-torrent": "^11.0.17",
|
"parse-torrent": "^11.0.17",
|
||||||
"piscina": "^4.7.0",
|
|
||||||
"rc-virtual-list": "^3.16.1",
|
"rc-virtual-list": "^3.16.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
"react-redux": "^9.1.1",
|
"react-redux": "^9.1.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
|
"react-shadow": "^20.6.0",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.0",
|
||||||
"sound-play": "^1.1.0",
|
"sound-play": "^1.1.0",
|
||||||
|
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
|
||||||
"sudo-prompt": "^9.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"tough-cookie": "^5.1.1",
|
"tough-cookie": "^5.1.1",
|
||||||
"user-agents": "^1.1.387",
|
"user-agents": "^1.1.387",
|
||||||
|
"winreg": "^1.2.5",
|
||||||
|
"ws": "^8.18.1",
|
||||||
"yaml": "^2.6.1",
|
"yaml": "^2.6.1",
|
||||||
"yup": "^1.5.0",
|
"yup": "^1.5.0",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
@@ -85,6 +90,7 @@
|
|||||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
|
"@protobuf-ts/plugin": "^2.10.0",
|
||||||
"@swc/core": "^1.4.16",
|
"@swc/core": "^1.4.16",
|
||||||
"@types/auto-launch": "^5.0.5",
|
"@types/auto-launch": "^5.0.5",
|
||||||
"@types/color": "^3.0.6",
|
"@types/color": "^3.0.6",
|
||||||
@@ -97,6 +103,8 @@
|
|||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/sound-play": "^1.1.3",
|
"@types/sound-play": "^1.1.3",
|
||||||
"@types/user-agents": "^1.0.4",
|
"@types/user-agents": "^1.0.4",
|
||||||
|
"@types/winreg": "^1.2.36",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^31.7.7",
|
"electron": "^31.7.7",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
|
|||||||
1
proto
Submodule
1
proto
Submodule
Submodule proto added at 7a23620f93
@@ -3,7 +3,6 @@ const tar = require("tar");
|
|||||||
const util = require("node:util");
|
const util = require("node:util");
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const { spawnSync } = require("node:child_process");
|
|
||||||
|
|
||||||
const exec = util.promisify(require("node:child_process").exec);
|
const exec = util.promisify(require("node:child_process").exec);
|
||||||
|
|
||||||
@@ -15,8 +14,18 @@ const fileName = {
|
|||||||
darwin: `ludusavi-v${ludusaviVersion}-mac.tar.gz`,
|
darwin: `ludusavi-v${ludusaviVersion}-mac.tar.gz`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ludusaviBinaryName = {
|
||||||
|
win32: "ludusavi.exe",
|
||||||
|
linux: "ludusavi",
|
||||||
|
darwin: "ludusavi",
|
||||||
|
};
|
||||||
|
|
||||||
const downloadLudusavi = async () => {
|
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...");
|
console.log("Ludusavi already exists, skipping download...");
|
||||||
return;
|
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();
|
downloadLudusavi();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const s3 = new S3Client({
|
|||||||
|
|
||||||
const dist = path.resolve(__dirname, "..", "dist");
|
const dist = path.resolve(__dirname, "..", "dist");
|
||||||
|
|
||||||
const extensionsToUpload = [".deb", ".exe", ".pacman"];
|
const extensionsToUpload = [".deb", ".exe", ".pacman", ".AppImage"];
|
||||||
|
|
||||||
fs.readdir(dist, async (err, files) => {
|
fs.readdir(dist, async (err, files) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
1
sonar-project.properties
Normal file
1
sonar-project.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sonar.exclusions=src/main/generated/**
|
||||||
@@ -1,88 +1,88 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Български",
|
"language_name": "Български",
|
||||||
"app": {
|
"app": {
|
||||||
"successfully_signed_in": "Успешно вписване"
|
"successfully_signed_in": "Успешно влизане"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Препоръчани",
|
"featured": "Препоръчани",
|
||||||
"surprise_me": "Изненадай ме",
|
"surprise_me": "Изненадай ме",
|
||||||
"no_results": "Не са намерени резултати",
|
"no_results": "Няма намерени резултати",
|
||||||
"start_typing": "Търсене...",
|
"start_typing": "Започнете да пишете за търсене...",
|
||||||
"hot": "Актуално сега",
|
"hot": "Горещи сега",
|
||||||
"weekly": "📅 Най-доброто от седмицата",
|
"weekly": "📅 Топ игри на седмицата",
|
||||||
"achievements": "🏆 Игри, които да победите"
|
"achievements": "🏆 Игри които да победите"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Каталог",
|
"catalogue": "Каталог",
|
||||||
"downloads": "Изтегляния",
|
"downloads": "Изтегляния",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"my_library": "Моята библиотека",
|
"my_library": "Моята библиотека",
|
||||||
"downloading_metadata": "{{title}} (Сваляне на метаданни…)",
|
"downloading_metadata": "{{title}} (Изтегляне на метаданни…)",
|
||||||
"paused": "{{title}} (Пауза)",
|
"paused": "{{title}} (На пауза)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Изтегляне…)",
|
"downloading": "{{title}} ({{percentage}} - Изтегляне…)",
|
||||||
"filter": "Търсене по име",
|
"filter": "Филтрирай библиотеката",
|
||||||
"home": "Начало",
|
"home": "Начало",
|
||||||
"queued": "{{title}} (Опашка)",
|
"queued": "{{title}} (В опашката)",
|
||||||
"game_has_no_executable": "Играта няма избран изпълним файл",
|
"game_has_no_executable": "Няма избран изпълним файл за играта",
|
||||||
"sign_in": "Вписване",
|
"sign_in": "Вход",
|
||||||
"friends": "Приятели",
|
"friends": "Приятели",
|
||||||
"need_help": "Имате нужда от помощ??",
|
"need_help": "Нужда от помощ?",
|
||||||
"favorites": "Любими игри"
|
"favorites": "Любими"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Търсене",
|
"search": "Търси игри",
|
||||||
"home": "Начало",
|
"home": "Начало",
|
||||||
"catalogue": "Каталог",
|
"catalogue": "Каталог",
|
||||||
"downloads": "Изтегляния",
|
"downloads": "Изтегляния",
|
||||||
"search_results": "Резултати от търсене",
|
"search_results": "Резултати от търсенето",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"version_available_install": "Версия {{version}} е налична. Кликни тук, за да рестартирате и инсталирате.",
|
"version_available_install": "Версия {{version}} е налична. Кликнете тук за рестарт и инсталация.",
|
||||||
"version_available_download": "Версия {{version}} е налична. Кликни тук за изтегляне."
|
"version_available_download": "Версия {{version}} е налична. Кликнете тук за изтегляне."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Няма изтегляния в ход",
|
"no_downloads_in_progress": "Няма текущи изтегляния",
|
||||||
"downloading_metadata": "Сваляне на {{title}} метадата…",
|
"downloading_metadata": "Изтегляне на метаданни за {{title}}…",
|
||||||
"downloading": "Изтегляне на {{title}}… ({{percentage}} готово) - Остават {{eta}} - {{speed}}",
|
"downloading": "Изтегля се {{title}}… ({{percentage}} завършено) - Завършване {{eta}} - {{speed}}",
|
||||||
"calculating_eta": "Изтегляне на {{title}}… ({{percentage}} готово) - Изчисляване на оставащо време…",
|
"calculating_eta": "Изтегля се {{title}}… ({{percentage}} завършено) - Изчисляване на оставащо време…",
|
||||||
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
|
"checking_files": "Проверка на файловете за {{title}}… ({{percentage}} завършено)",
|
||||||
|
"installing_common_redist": "{{log}}…",
|
||||||
|
"installation_complete": "Инсталацията завършена",
|
||||||
|
"installation_complete_message": "Общите компоненти са инсталирани успешно"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"search": "Филтър…",
|
"search": "Филтрирай…",
|
||||||
"developers": "Разработчици",
|
"developers": "Разработчици",
|
||||||
"genres": "Жанрове",
|
"genres": "Жанрове",
|
||||||
"tags": "Тагове",
|
"tags": "Тагове",
|
||||||
"publishers": "Издатели",
|
"publishers": "Издатели",
|
||||||
"download_sources": "Източници за изтегляне",
|
"download_sources": "Източници за изтегляне",
|
||||||
"result_count": "{{resultCount}} резултати",
|
"result_count": "{{resultCount}} резултата",
|
||||||
"filter_count": "{{filterCount}} налични",
|
"filter_count": "{{filterCount}} налични",
|
||||||
"clear_filters": "Изчисти {{filterCount}} избрани"
|
"clear_filters": "Изчисти {{filterCount}} избрани"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"launch_options": "Опции за стартиране",
|
"open_download_options": "Отвори опциите за изтегляне",
|
||||||
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране (экспериментальный)",
|
"download_options_zero": "Няма опции за изтегляне",
|
||||||
"launch_options_placeholder": "Няма зададен параметър",
|
"download_options_one": "{{count}} опция за изтегляне",
|
||||||
"open_download_options": "Варианти за изтегляне",
|
"download_options_other": "{{count}} опции за изтегляне",
|
||||||
"download_options_zero": "Няма варианти за изтегляне",
|
|
||||||
"download_options_one": "{{count}} варианти за изтегляне",
|
|
||||||
"download_options_other": "{{count}} варианти за изтегляне",
|
|
||||||
"updated_at": "Обновено на {{updated_at}}",
|
"updated_at": "Обновено на {{updated_at}}",
|
||||||
"install": "Инсталирай",
|
"install": "Инсталирай",
|
||||||
"resume": "Продължи",
|
"resume": "Продължи",
|
||||||
"pause": "Пауза",
|
"pause": "Пауза",
|
||||||
"cancel": "Отказ",
|
"cancel": "Отказ",
|
||||||
"remove": "Премахни",
|
"remove": "Премахни",
|
||||||
"space_left_on_disk": "{{space}} място на диска",
|
"space_left_on_disk": "{{space}} свободно на диска",
|
||||||
"eta": "Заклчение {{eta}}",
|
"eta": "Завършване {{eta}}",
|
||||||
"calculating_eta": "Калкулиране на оставащо време…",
|
"calculating_eta": "Изчисляване на оставащо време…",
|
||||||
"downloading_metadata": "Изтегляне на метадата…",
|
"downloading_metadata": "Изтегляне на метаданни…",
|
||||||
"filter": "Филтрирай repacks",
|
"filter": "Филтрирай репаковки",
|
||||||
"requirements": "Системни изисквания",
|
"requirements": "Системни изисквания",
|
||||||
"minimum": "Минимални",
|
"minimum": "Минимални",
|
||||||
"recommended": "Препоръчителни",
|
"recommended": "Препоръчителни",
|
||||||
"paused": "Паузирано",
|
"paused": "На пауза",
|
||||||
"release_date": "Издадено на {{date}}",
|
"release_date": "Издадена на {{date}}",
|
||||||
"publisher": "Публикувано от {{publisher}}",
|
"publisher": "Издател: {{publisher}}",
|
||||||
"hours": "часове",
|
"hours": "часа",
|
||||||
"minutes": "минути",
|
"minutes": "минути",
|
||||||
"amount_hours": "{{amount}} часа",
|
"amount_hours": "{{amount}} часа",
|
||||||
"amount_minutes": "{{amount}} минути",
|
"amount_minutes": "{{amount}} минути",
|
||||||
@@ -90,333 +90,425 @@
|
|||||||
"add_to_library": "Добави в библиотеката",
|
"add_to_library": "Добави в библиотеката",
|
||||||
"remove_from_library": "Премахни от библиотеката",
|
"remove_from_library": "Премахни от библиотеката",
|
||||||
"no_downloads": "Няма налични изтегляния",
|
"no_downloads": "Няма налични изтегляния",
|
||||||
"play_time": "Игрално време {{amount}}",
|
"play_time": "Играно: {{amount}}",
|
||||||
"last_time_played": "Последно пускане {{period}}",
|
"last_time_played": "Последно играно: {{period}}",
|
||||||
"not_played_yet": "Не сте играли {{title}} все още",
|
"not_played_yet": "Все още не сте играли {{title}}",
|
||||||
"next_suggestion": "Следващо предложение",
|
"next_suggestion": "Следващо предложение",
|
||||||
"play": "Пускане",
|
"play": "Играй",
|
||||||
"deleting": "Изтриване на инсталация…",
|
"deleting": "Изтриване на инсталатора…",
|
||||||
"close": "Затвори",
|
"close": "Затвори",
|
||||||
"playing_now": "Играй сега",
|
"playing_now": "Играе се сега",
|
||||||
"change": "Промяна",
|
"change": "Промени",
|
||||||
"repacks_modal_description": "Избери repack който искаш да изтеглиш",
|
"repacks_modal_description": "Изберете репак за изтегляне",
|
||||||
"select_folder_hint": "За да промените стандартната папка отидете в <0>Настройки</0>",
|
"select_folder_hint": "За да промените папката по подразбиране, отидете в <0>Настройки</0>",
|
||||||
"download_now": "Изтегли сега",
|
"download_now": "Изтегли сега",
|
||||||
"no_shop_details": "Не може да се извлекат данни за магазина.",
|
"no_shop_details": "Неуспешно извличане на детайли от магазина.",
|
||||||
"download_options": "Опции за сваляне",
|
"download_options": "Опции за изтегляне",
|
||||||
"download_path": "Път за сваляне",
|
"download_path": "Път за изтегляне",
|
||||||
"previous_screenshot": "Предишна снимка",
|
"previous_screenshot": "Предишен скрийншот",
|
||||||
"next_screenshot": "Следваща снимка",
|
"next_screenshot": "Следващ скрийншот",
|
||||||
"screenshot": "Снимка {{number}}",
|
"screenshot": "Скрийншот {{number}}",
|
||||||
"open_screenshot": "Отвори снимки {{number}}",
|
"open_screenshot": "Отвори скрийншот {{number}}",
|
||||||
"download_settings": "Настройки за сваляне",
|
"download_settings": "Настройки за изтегляне",
|
||||||
"downloader": "Downloader",
|
"downloader": "Изтегляч",
|
||||||
"select_executable": "Избери",
|
"select_executable": "Избери",
|
||||||
"no_executable_selected": "Няма избран стартиращ файл",
|
"no_executable_selected": "Няма избран изпълним файл",
|
||||||
"open_folder": "Отвори папка",
|
"open_folder": "Отвори папка",
|
||||||
"open_download_location": "Виж свалените файлове",
|
"open_download_location": "Виж изтеглените файлове",
|
||||||
"create_shortcut": "Пряк път на Десктопа",
|
"create_shortcut": "Създай пряк път на работния плот",
|
||||||
|
"clear": "Изчисти",
|
||||||
"remove_files": "Премахни файловете",
|
"remove_files": "Премахни файловете",
|
||||||
"remove_from_library_title": "Сигурен ли си?",
|
"remove_from_library_title": "Сигурни ли сте?",
|
||||||
"remove_from_library_description": "Това ще премахне {{game}} от Библиотеката",
|
"remove_from_library_description": "Това ще премахне {{game}} от вашата библиотека",
|
||||||
"options": "Опции",
|
"options": "Опции",
|
||||||
"executable_section_title": "Стартиращ файл",
|
"executable_section_title": "Изпълним файл",
|
||||||
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Пускане\"",
|
"executable_section_description": "Пътят на файла, който ще се изпълни при \"Играй\"",
|
||||||
"downloads_section_title": "Свалени",
|
"downloads_section_title": "Изтегляния",
|
||||||
"downloads_section_description": "Вижте актуализации или други версии на тази игра",
|
"downloads_section_description": "Вижте обновления или други версии на тази игра",
|
||||||
"danger_zone_section_title": "Опасна зона",
|
"danger_zone_section_title": "Опасна зона",
|
||||||
"danger_zone_section_description": "Премахнете тази игра от библиотеката си или от файловете, изтеглени от Hydra",
|
"danger_zone_section_description": "Премахнете тази игра от библиотеката или файловете, изтеглени от Hydra",
|
||||||
"download_in_progress": "Изтегляне в ход",
|
"download_in_progress": "Изтеглянето е в ход",
|
||||||
"download_paused": "Изтеглянето е паузирано",
|
"download_paused": "Изтеглянето е на пауза",
|
||||||
"last_downloaded_option": "Опция от последно изтегляне",
|
"last_downloaded_option": "Последно изтеглена опция",
|
||||||
|
"create_steam_shortcut": "Създай пряк път за Steam",
|
||||||
"create_shortcut_success": "Прекият път е създаден успешно",
|
"create_shortcut_success": "Прекият път е създаден успешно",
|
||||||
"create_shortcut_error": "Грешка при създаването на пряк път",
|
"you_might_need_to_restart_steam": "Може да е необходимо да рестартирате Steam, за да видите промените",
|
||||||
|
"create_shortcut_error": "Грешка при създаване на пряк път",
|
||||||
"nsfw_content_title": "Тази игра съдържа неподходящо съдържание",
|
"nsfw_content_title": "Тази игра съдържа неподходящо съдържание",
|
||||||
"nsfw_content_description": "{{title}} съдържа съдържание, което може да не е подходящо за всички възрасти. Сигурни ли сте, че искате да продължите?",
|
"nsfw_content_description": "{{title}} съдържа съдържание, което може да не е подходящо за всички възрасти. Сигурни ли сте, че искате да продължите?",
|
||||||
"allow_nsfw_content": "Продължи",
|
"allow_nsfw_content": "Продължи",
|
||||||
"refuse_nsfw_content": "Назад",
|
"refuse_nsfw_content": "Върни се",
|
||||||
"stats": "Статистики",
|
"stats": "Статистики",
|
||||||
"download_count": "Сваляния",
|
"download_count": "Изтегляния",
|
||||||
"player_count": "Активни играчи",
|
"player_count": "Активни играчи",
|
||||||
"download_error": "Тази опция за изтегляне не е налична",
|
"download_error": "Тази опция за изтегляне не е налична",
|
||||||
"download": "Свали",
|
"download": "Изтегли",
|
||||||
"executable_path_in_use": "Изпълнимият файл вече се използва от \"{{game}}\"",
|
"executable_path_in_use": "Изпълнимият файл вече се използва от \"{{game}}\"",
|
||||||
"warning": "Внимание:",
|
"warning": "Внимание:",
|
||||||
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена, когато е завършено. Ако Hydra се затвори преди завършването, ще загубите напредъка си..",
|
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена до завършване. Ако затворите преди завършване, ще загубите прогреса.",
|
||||||
"achievements": "Постижения",
|
"achievements": "Постижения",
|
||||||
"achievements_count": "Постижения {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "Постижения {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "Запазване в облака",
|
"cloud_save": "Облачно запазване",
|
||||||
"cloud_save_description": "Запазете напредъка си в облака и продължете да играете на всяко устройство",
|
"cloud_save_description": "Запазете прогреса си в облака и продължете да играете на всяко устройство",
|
||||||
"backups": "Резервни копия",
|
"backups": "Архиви",
|
||||||
"install_backup": "Инсталирай",
|
"install_backup": "Инсталирай",
|
||||||
"delete_backup": "Изтрий",
|
"delete_backup": "Изтрий",
|
||||||
"create_backup": "Ново копие",
|
"create_backup": "Нов архив",
|
||||||
"last_backup_date": "Последно копие от {{date}}",
|
"last_backup_date": "Последен архив на {{date}}",
|
||||||
"no_backup_preview": "Не бяха намерени запазени игри за това заглавие",
|
"no_backup_preview": "Не са намерени запазени игри за това заглавие",
|
||||||
"restoring_backup": "Възстановяване на резервно копие ({{progress}} готово)…",
|
"restoring_backup": "Възстановяване на архив ({{progress}} завършено)…",
|
||||||
"uploading_backup": "Качване на резервно копие…",
|
"uploading_backup": "Качване на архив…",
|
||||||
"no_backups": "Все още не сте създали резервни копия за тази игра",
|
"no_backups": "Не сте създали архиви за тази игра",
|
||||||
"backup_uploaded": "Качено резервно копие",
|
"backup_uploaded": "Архивът е качен",
|
||||||
"backup_deleted": "Изтрито резервно копие",
|
"backup_deleted": "Архивът е изтрит",
|
||||||
"backup_restored": "Възстановен бекъп",
|
"backup_restored": "Архивът е възстановен",
|
||||||
"see_all_achievements": "Вижте всички постижения",
|
"see_all_achievements": "Виж всички постижения",
|
||||||
"sign_in_to_see_achievements": "Влезте, за да видите постиженията",
|
"sign_in_to_see_achievements": "Влезте, за да видите постиженията",
|
||||||
"mapping_method_automatic": "Автоматично",
|
"mapping_method_automatic": "Автоматично",
|
||||||
"mapping_method_manual": "Ръчно",
|
"mapping_method_manual": "Ръчно",
|
||||||
"mapping_method_label": "Метод на картографиране",
|
"mapping_method_label": "Метод на съпоставяне",
|
||||||
"files_automatically_mapped": "Автоматично картографиране на файлове",
|
"files_automatically_mapped": "Файловете са съпоставени автоматично",
|
||||||
"no_backups_created": "Не са създадени резервни копия за тази игра",
|
"no_backups_created": "Няма създадени архиви за тази игра",
|
||||||
"manage_files": "Управление на файлове",
|
"manage_files": "Управлявай файлове",
|
||||||
"loading_save_preview": "Търсене на запазени игри…",
|
"loading_save_preview": "Търсене на запазени игри…",
|
||||||
"wine_prefix": "Wine Префикс",
|
"wine_prefix": "Wine префикс",
|
||||||
"wine_prefix_description": "Wine prefix използван за тази игра",
|
"wine_prefix_description": "Wine префикс, използван за стартиране на тази игра",
|
||||||
"no_download_option_info": "Няма налични данни",
|
"launch_options": "Опции за стартиране",
|
||||||
"backup_deletion_failed": "Неуспешно изтриване на резервно копие",
|
"launch_options_description": "Напреднали потребители могат да въведат модификации (експериментална функция)",
|
||||||
"max_number_of_artifacts_reached": "Достигнат максимален брой резервни копия за тази игра",
|
"launch_options_placeholder": "Няма зададен параметър",
|
||||||
"achievements_not_sync": "Постиженията не са синхронизирани",
|
"no_download_option_info": "Няма налична информация",
|
||||||
"manage_files_description": "Управлявайте кои файлове ще бъдат архивирани и възстановени",
|
"backup_deletion_failed": "Неуспешно изтриване на архив",
|
||||||
|
"max_number_of_artifacts_reached": "Достигнат е максималният брой архиви за тази игра",
|
||||||
|
"achievements_not_sync": "Вижте как да синхронизирате постиженията си",
|
||||||
|
"manage_files_description": "Управлявайте кои файлове ще се архивират и възстановяват",
|
||||||
"select_folder": "Избери папка",
|
"select_folder": "Избери папка",
|
||||||
"backup_from": "Резервно копие от {{date}}",
|
"backup_from": "Архив от {{date}}",
|
||||||
"custom_backup_location_set": "Задаване на персонализирано местоположение за архивиране"
|
"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": {
|
"activation": {
|
||||||
"title": "Активирай Hydra",
|
"title": "Активирай Hydra",
|
||||||
"installation_id": "Идентификатор на инсталацията:",
|
"installation_id": "Инсталационен ID:",
|
||||||
"enter_activation_code": "Въведете кода за активиране",
|
"enter_activation_code": "Въведете активационен код",
|
||||||
"message": "Ако не знаете къде да попитате за това, значи не трябва да го имате..",
|
"message": "Ако не знаете къде да попитате за това, не бива да го имате.",
|
||||||
"activate": "Активирай",
|
"activate": "Активирай",
|
||||||
"loading": "Зареждане…"
|
"loading": "Зареждане…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"seeding": "Сийдване",
|
|
||||||
"stop_seeding": "Спри сийдването",
|
|
||||||
"resume_seeding": "Продължи сийдването",
|
|
||||||
"options": "Управление",
|
|
||||||
"resume": "Продължи",
|
"resume": "Продължи",
|
||||||
"pause": "Пауза",
|
"pause": "Пауза",
|
||||||
"eta": "Conclusion {{eta}}",
|
"eta": "Завършване {{eta}}",
|
||||||
"paused": "Паузирано",
|
"paused": "На пауза",
|
||||||
"verifying": "Проверка…",
|
"verifying": "Проверка…",
|
||||||
"completed": "Готово",
|
"completed": "Завършено",
|
||||||
"removed": "Не е изтеглен",
|
"removed": "Не е изтеглено",
|
||||||
"cancel": "Отказ",
|
"cancel": "Отказ",
|
||||||
"filter": "Филтриране на изтеглени игри",
|
"filter": "Филтрирай изтеглените игри",
|
||||||
"remove": "Премахни",
|
"remove": "Премахни",
|
||||||
"downloading_metadata": "Изтегляне на метаданни…",
|
"downloading_metadata": "Изтегляне на метаданни…",
|
||||||
"deleting": "Изтриване на инсталатора…",
|
"deleting": "Изтриване на инсталатора…",
|
||||||
"delete": "Премахване на инсталатора",
|
"delete": "Премахни инсталатора",
|
||||||
"delete_modal_title": "Сигурени ли сте?",
|
"delete_modal_title": "Сигурни ли сте?",
|
||||||
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви.",
|
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви",
|
||||||
"install": "Инсталирай",
|
"install": "Инсталирай",
|
||||||
"download_in_progress": "В процес на изпълнение",
|
"download_in_progress": "В процес",
|
||||||
"queued_downloads": "Изтеглени файлове в опашката",
|
"queued_downloads": "Изтегляния на опашка",
|
||||||
"downloads_completed": "Приключени",
|
"downloads_completed": "Завършени",
|
||||||
"queued": "В опашка",
|
"queued": "В опашката",
|
||||||
"no_downloads_title": "Толкова е празно",
|
"no_downloads_title": "Толкова е празно",
|
||||||
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете...",
|
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете.",
|
||||||
"checking_files": "Проверка на файлове…"
|
"checking_files": "Проверка на файлове…",
|
||||||
|
"seeding": "Сийдване",
|
||||||
|
"stop_seeding": "Спри сийдването",
|
||||||
|
"resume_seeding": "Продължи сийдването",
|
||||||
|
"options": "Управлявай",
|
||||||
|
"extract": "Извлечи файловете",
|
||||||
|
"extracting": "Извличане на файловете…"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"seed_after_download_complete": "Сийд след завършване на изтеглянето",
|
"downloads_path": "Път за изтегляния",
|
||||||
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключването им",
|
"change": "Обнови",
|
||||||
"downloads_path": "Инсталационен път",
|
|
||||||
"change": "Актуализиране",
|
|
||||||
"notifications": "Известия",
|
"notifications": "Известия",
|
||||||
"enable_download_notifications": "Когато изтеглянето е завършено",
|
"enable_download_notifications": "Когато изтеглянето приключи",
|
||||||
"enable_repack_list_notifications": "Когато се добави нов repack",
|
"enable_repack_list_notifications": "Когато бъде добавен нов репак",
|
||||||
"real_debrid_api_token_label": "Real-Debrid API токен",
|
"real_debrid_api_token_label": "Real-Debrid API токен",
|
||||||
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
|
"quit_app_instead_hiding": "Не скривай Hydra при затваряне",
|
||||||
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
|
"launch_with_system": "Стартирай Hydra при стартиране на системата",
|
||||||
"general": "Общи",
|
"general": "Общи",
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Източници за изтегляне",
|
"download_sources": "Източници за изтегляне",
|
||||||
"language": "Език",
|
"language": "Език",
|
||||||
"api_token": "API Токен",
|
"api_token": "API токен",
|
||||||
"enable_real_debrid": "Включи Real-Debrid",
|
"enable_real_debrid": "Включи Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
"real_debrid_description": "Real-Debrid е неограничен изтегляч, който ви позволява да теглите бързо, ограничено само от интернет връзката ви.",
|
||||||
"debrid_invalid_token": "Невалиден API токен",
|
"debrid_invalid_token": "Невалиден API токен",
|
||||||
"debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
"debrid_api_token_hint": "Може да получите вашия API токен <0>тук</0>",
|
||||||
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен. Моля, абонирайте се за Real-Debrid",
|
||||||
"debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
"debrid_linked_message": "Акаунт \"{{username}}\" е свързан",
|
||||||
"save_changes": "Запази промените",
|
"save_changes": "Запази промените",
|
||||||
"changes_saved": "Промените са успешно запазни",
|
"changes_saved": "Промените са запазени успешно",
|
||||||
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
"download_sources_description": "Hydra ще взема линкове за изтегляне от тези източници. URL адресът трябва да сочи към .json файл с линкове.",
|
||||||
"validate_download_source": "Валидиране",
|
"validate_download_source": "Валидирай",
|
||||||
"remove_download_source": "Премахни",
|
"remove_download_source": "Премахни",
|
||||||
"add_download_source": "Добави източник",
|
"add_download_source": "Добави източник",
|
||||||
"download_count_zero": "Няма опции за сваляне",
|
"download_count_zero": "Няма опции за изтегляне",
|
||||||
"download_count_one": "{{countFormatted}} опции за сваляне",
|
"download_count_one": "{{countFormatted}} опция за изтегляне",
|
||||||
"download_count_other": "{{countFormatted}} опции за сваляне",
|
"download_count_other": "{{countFormatted}} опции за изтегляне",
|
||||||
"download_source_url": "URL адрес на източника за изтегляне",
|
"download_source_url": "URL на източника",
|
||||||
"add_download_source_description": "Вмъкнете URL адреса на файла .json",
|
"add_download_source_description": "Въведете URL на .json файла",
|
||||||
"download_source_up_to_date": "Актуален",
|
"download_source_up_to_date": "Актуализиран",
|
||||||
"download_source_errored": "Сгрешен",
|
"download_source_errored": "Грешка",
|
||||||
"sync_download_sources": "Синхронизирай източниците",
|
"sync_download_sources": "Синхронизирай източници",
|
||||||
"removed_download_source": "Източника за сваляне е премахнат",
|
"removed_download_source": "Източникът е премахнат",
|
||||||
"cancel_button_confirmation_delete_all_sources": "не",
|
"removed_download_sources": "Източниците са премахнати",
|
||||||
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
"cancel_button_confirmation_delete_all_sources": "Не",
|
||||||
"description_confirmation_delete_all_sources": "Вы удалите все источники загрузки",
|
"confirm_button_confirmation_delete_all_sources": "Да, изтрий всичко",
|
||||||
"title_confirmation_delete_all_sources": "Удалить все источники загрузки",
|
"title_confirmation_delete_all_sources": "Изтрий всички източници",
|
||||||
"removed_download_sources": "Шрифты удалены",
|
"description_confirmation_delete_all_sources": "Ще изтриете всички източници",
|
||||||
"button_delete_all_sources": "Удалить все источники загрузки",
|
"button_delete_all_sources": "Премахни всички",
|
||||||
"added_download_source": "Добавен източник за сваляне",
|
"added_download_source": "Източникът е добавен",
|
||||||
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
|
"download_sources_synced": "Всички източници са синхронизирани",
|
||||||
"insert_valid_json_url": "Добавете ваиден JSON линк",
|
"insert_valid_json_url": "Въведете валиден JSON url",
|
||||||
"found_download_option_zero": "Няма намерени опции за сваляне",
|
"found_download_option_zero": "Не е намерена опция за изтегляне",
|
||||||
"found_download_option_one": "Намерени {{countFormatted}} опции за сваляне",
|
"found_download_option_one": "Намерена е {{countFormatted}} опция за изтегляне",
|
||||||
"found_download_option_other": "Намерени {{countFormatted}} опции за сваляне",
|
"found_download_option_other": "Намерени са {{countFormatted}} опции за изтегляне",
|
||||||
"import": "Внеси",
|
"import": "Импортирай",
|
||||||
"public": "Публичен",
|
"public": "Публично",
|
||||||
"private": "Личен",
|
"private": "Частно",
|
||||||
"friends_only": "Само за приятели",
|
"friends_only": "Само за приятели",
|
||||||
"privacy": "Поверителност",
|
"privacy": "Поверителност",
|
||||||
"profile_visibility": "Видимост на профила",
|
"profile_visibility": "Видимост на профила",
|
||||||
"profile_visibility_description": "Изберете кой може да вижда вашия профил и библиотека",
|
"profile_visibility_description": "Изберете кой може да вижда вашия профил и библиотека",
|
||||||
"required_field": "Това поле е задължително",
|
"required_field": "Това поле е задължително",
|
||||||
"source_already_exists": "Този източник вече е добавен",
|
"source_already_exists": "Този източник вече е добавен",
|
||||||
"must_be_valid_url": "Източникът трябва да е валиден URL адрес.",
|
"must_be_valid_url": "Източникът трябва да е валиден URL",
|
||||||
"blocked_users": "Блокирани потребители",
|
"blocked_users": "Блокирани потребители",
|
||||||
"user_unblocked": "Потребителят е бил деблокиран",
|
"user_unblocked": "Потребителят е деблокиран",
|
||||||
"enable_achievement_notifications": "Когато е отключено постижение",
|
"enable_achievement_notifications": "Когато бъде отключено постижение",
|
||||||
"launch_minimized": "Стартиране на Hydra минимизирано",
|
"launch_minimized": "Стартирай Hydra минимизирано",
|
||||||
"disable_nsfw_alert": "Деактивиране на предупреждението NSFW"
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Изтеглянето е завършено",
|
"download_complete": "Изтеглянето завърши",
|
||||||
"game_ready_to_install": "{{title}} е готово за инсталиране",
|
"game_ready_to_install": "{{title}} е готова за инсталация",
|
||||||
"repack_list_updated": "Repack лист е обновен",
|
"repack_list_updated": "Списъкът с репаци е обновен",
|
||||||
"repack_count_one": "{{count}} repack е добавен",
|
"repack_count_one": "Добавен е {{count}} репак",
|
||||||
"repack_count_other": "{{count}} repacks добавени",
|
"repack_count_other": "Добавени са {{count}} репака",
|
||||||
"new_update_available": "Версия {{version}} е налична",
|
"new_update_available": "Налична е версия {{version}}",
|
||||||
"restart_to_install_update": "Рестартирайте Hydra, за да инсталирате актуализацията",
|
"restart_to_install_update": "Рестартирайте Hydra за инсталиране на обновлението",
|
||||||
"notification_achievement_unlocked_title": "Отключено постижение за {{game}}",
|
"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": {
|
"system_tray": {
|
||||||
"open": "Отвори Hydra",
|
"open": "Отвори Hydra",
|
||||||
"quit": "Изход"
|
"quit": "Изход"
|
||||||
},
|
},
|
||||||
"game_card": {
|
"game_card": {
|
||||||
|
"available_one": "Налично",
|
||||||
|
"available_other": "Налично",
|
||||||
"no_downloads": "Няма налични изтегляния"
|
"no_downloads": "Няма налични изтегляния"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Не инсталирани програми",
|
"title": "Програмите не са инсталирани",
|
||||||
"description": "Wine или Lutris изпълними файлове не бяха открити на вашата система",
|
"description": "Wine или Lutris не са открити на вашата система",
|
||||||
"instructions": "Проверете правилния начин за инсталиране на някоя от тях на вашата дистрибуция на Linux, за да може играта да работи нормално"
|
"instructions": "Проверете как да инсталирате някоя от тях за вашата Linux дистрибуция, за да може играта да работи."
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Бутон за затваряне"
|
"close": "Бутон за затваряне"
|
||||||
},
|
},
|
||||||
"forms": {
|
"forms": {
|
||||||
"toggle_password_visibility": "Превключване на видимостта на паролата"
|
"toggle_password_visibility": "Показване/скриване на паролата"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"stats": "Статистики",
|
"amount_hours": "{{amount}} часа",
|
||||||
"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_minutes": "{{amount}} минути",
|
"amount_minutes": "{{amount}} минути",
|
||||||
"last_time_played": "Последно играно {{period}}",
|
"last_time_played": "Последно играно: {{period}}",
|
||||||
"activity": "Скорошна активност",
|
"activity": "Последна активност",
|
||||||
"library": "Библиотека",
|
"library": "Библиотека",
|
||||||
"total_play_time": "Общо време за игра",
|
"total_play_time": "Общо време за игра",
|
||||||
"no_recent_activity_title": "Хмм… няма нищо тук",
|
"no_recent_activity_title": "Хммм… няма нищо тук",
|
||||||
"no_recent_activity_description": "Не сте играли игри напоследък. Време е да промените това.!",
|
"no_recent_activity_description": "Не сте играли игри наскоро. Време е да го промените!",
|
||||||
"display_name": "Показване на името",
|
"display_name": "Показвано име",
|
||||||
"saving": "Запазване",
|
"saving": "Запазване",
|
||||||
"save": "Запис",
|
"save": "Запази",
|
||||||
"edit_profile": "Редактиране на профила",
|
"edit_profile": "Редактирай профил",
|
||||||
"saved_successfully": "Запазено успешно",
|
"saved_successfully": "Успешно запазено",
|
||||||
"try_again": "Моля, опитайте пак",
|
"try_again": "Моля, опитайте отново",
|
||||||
"sign_out_modal_title": "Сигурни ли сте?",
|
"sign_out_modal_title": "Сигурни ли сте?",
|
||||||
"cancel": "Отказ",
|
"cancel": "Отказ",
|
||||||
"successfully_signed_out": "Успешно се отписахте",
|
"successfully_signed_out": "Успешно излязохте",
|
||||||
"sign_out": "Отписване",
|
"sign_out": "Изход",
|
||||||
"playing_for": "В игра от {{amount}}",
|
"playing_for": "Играе се от {{amount}}",
|
||||||
"sign_out_modal_text": "Вашата библиотека е свързана с текущата ви сметка. Когато се отпишете, библиотеката ви вече няма да е видима и напредъкът няма да бъде запазен. Продължете с отписването?",
|
"sign_out_modal_text": "Библиотеката ви е свързана с този акаунт. При изход, тя няма да е видима, а прогресът няма да се запази. Продължавате ли?",
|
||||||
"add_friends": "Добави приятели",
|
"add_friends": "Добави приятели",
|
||||||
"add": "Добави",
|
"add": "Добави",
|
||||||
"friend_code": "Приятелски код",
|
"friend_code": "Код за приятелство",
|
||||||
"see_profile": "Виж профила",
|
"see_profile": "Виж профила",
|
||||||
"sending": "Изпращане",
|
"sending": "Изпращане",
|
||||||
"friend_request_sent": "Изпратена покана за приятелство",
|
"friend_request_sent": "Заявката е изпратена",
|
||||||
"friends": "Приятели",
|
"friends": "Приятели",
|
||||||
"friends_list": "Списък с приятели",
|
"friends_list": "Списък с приятели",
|
||||||
"user_not_found": "Не е намерен потребител",
|
"user_not_found": "Потребителят не е намерен",
|
||||||
"block_user": "Блокирай потребител",
|
"block_user": "Блокирай потребител",
|
||||||
"add_friend": "Добави приятел",
|
"add_friend": "Добави приятел",
|
||||||
"request_sent": "Изпратена покана",
|
"request_sent": "Заявката е изпратена",
|
||||||
"request_received": "Получена покана",
|
"request_received": "Получена заявка",
|
||||||
"accept_request": "Приеми поканата",
|
"accept_request": "Приеми заявката",
|
||||||
"ignore_request": "Игнирирай поканата",
|
"ignore_request": "Игнорирай заявката",
|
||||||
"cancel_request": "Откажи поканата",
|
"cancel_request": "Отмени заявката",
|
||||||
"undo_friendship": "Отмяна на приятелството",
|
"undo_friendship": "Премахни приятелството",
|
||||||
"request_accepted": "Поканата е приета",
|
"request_accepted": "Заявката е приета",
|
||||||
"user_blocked_successfully": "Потребителят е блокиран успешно",
|
"user_blocked_successfully": "Потребителят е блокиран успешно",
|
||||||
"user_block_modal_text": "Това ще блокира {{displayName}}",
|
"user_block_modal_text": "Това ще блокира {{displayName}}",
|
||||||
"blocked_users": "Блокирани потребители",
|
"blocked_users": "Блокирани потребители",
|
||||||
"unblock": "Отблокирай",
|
"unblock": "Деблокирай",
|
||||||
"no_friends_added": "Не сте добавили приятели",
|
"no_friends_added": "Нямате добавени приятели",
|
||||||
"pending": "Чакащи",
|
"pending": "Чакащи",
|
||||||
"no_pending_invites": "Нямате чакащи покани",
|
"no_pending_invites": "Нямате чакащи покани",
|
||||||
"no_blocked_users": "Нямате блокирани потребители",
|
"no_blocked_users": "Нямате блокирани потребители",
|
||||||
"friend_code_copied": "Приятелския код е копиран",
|
"friend_code_copied": "Кодът за приятелство е копиран",
|
||||||
"undo_friendship_modal_text": "Това ще отмени приятелството ви с {{displayName}}",
|
"undo_friendship_modal_text": "Това ще премахне приятелството ви с {{displayName}}",
|
||||||
"privacy_hint": "За да настроите кой може да вижда това, отидете в <0>Настройки</0>",
|
"privacy_hint": "За да промените кой вижда това, отидете в <0>Настройки</0>",
|
||||||
"locked_profile": "Този профил е личен",
|
"locked_profile": "Този профил е частен",
|
||||||
"image_process_failure": "Грешка при обработката на изображението",
|
"image_process_failure": "Грешка при обработка на изображението",
|
||||||
"required_field": "Това поле е задължително",
|
"required_field": "Това поле е задължително",
|
||||||
"displayname_min_length": "Името трябва да е дълго поне 3 символа",
|
"displayname_min_length": "Показваното име трябва да съдържа поне 3 символа",
|
||||||
"displayname_max_length": "Името трябва да е с дължина не повече от 50 символа.",
|
"displayname_max_length": "Показваното име трябва да съдържа най-много 50 символа",
|
||||||
"report_profile": "Докладвай този профил",
|
"report_profile": "Докладвай този профил",
|
||||||
"report_reason": "Защо докладвате този профил?",
|
"report_reason": "Защо докладвате този профил?",
|
||||||
"report_description": "Допълнителна информация",
|
"report_description": "Допълнителна информация",
|
||||||
"report_description_placeholder": "Допълнителна информация",
|
"report_description_placeholder": "Допълнителна информация",
|
||||||
"report": "Докладвай",
|
"report": "Докладвай",
|
||||||
"report_reason_hate": "Омразна реч",
|
"report_reason_hate": "Реч на омразата",
|
||||||
"report_reason_sexual_content": "Сексуално съдържание",
|
"report_reason_sexual_content": "Сексуално съдържание",
|
||||||
"report_reason_violence": "Насилия",
|
"report_reason_violence": "Насилие",
|
||||||
"report_reason_spam": "Спам",
|
"report_reason_spam": "Спам",
|
||||||
"report_reason_other": "Друго",
|
"report_reason_other": "Друго",
|
||||||
"profile_reported": "Профилът е докладван",
|
"profile_reported": "Профилът е докладван",
|
||||||
"your_friend_code": "Вашия приятелски код:",
|
"your_friend_code": "Вашият код за приятелство:",
|
||||||
"upload_banner": "Качи банер",
|
"upload_banner": "Качи банер",
|
||||||
"uploading_banner": "Качване на банер…",
|
"uploading_banner": "Качване на банера…",
|
||||||
"background_image_updated": "Обновено фоново изображение"
|
"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": {
|
||||||
|
"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": "Това е скрито постижение",
|
"hidden_achievement_tooltip": "Това е скрито постижение",
|
||||||
"achievement_earn_points": "Спечели {{points}} точки с това постижение",
|
"achievement_earn_points": "Спечелете {{points}} точки с това постижение",
|
||||||
"earned_points": "Спечелени точки:",
|
"earned_points": "Спечелени точки:",
|
||||||
"available_points": "Налични точки:",
|
"available_points": "Налични точки:",
|
||||||
"how_to_earn_achievements_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}}"
|
|
||||||
},
|
},
|
||||||
"hydra_cloud": {
|
"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": "Hydra Cloud",
|
||||||
"hydra_cloud_feature_found": "Открихте функция на Hydra Cloud!",
|
"hydra_cloud_feature_found": "Открихте функция на Hydra Cloud!",
|
||||||
"learn_more": "Научете повече",
|
"learn_more": "Научете повече",
|
||||||
"subscription_tour_title": "Hydra Cloud Абонамент",
|
"debrid_description": "Изтегляйте до 4 пъти по-бързо с Nimbus"
|
||||||
"subscribe_now": "Абонирай се сега",
|
|
||||||
"cloud_saving": "Запазване в облака",
|
|
||||||
"cloud_achievements": "Запазете постиженията си в облака",
|
|
||||||
"animated_profile_picture": "Анимирана профилна снимка",
|
|
||||||
"premium_support": "Премиум поддръжка",
|
|
||||||
"show_and_compare_achievements": "Показвайте и сравнявайте постиженията си с тези на други потребители",
|
|
||||||
"animated_profile_banner": "Анимиран профилен банер"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,9 +130,11 @@
|
|||||||
"download_in_progress": "Download in progress",
|
"download_in_progress": "Download in progress",
|
||||||
"download_paused": "Download paused",
|
"download_paused": "Download paused",
|
||||||
"last_downloaded_option": "Last downloaded option",
|
"last_downloaded_option": "Last downloaded option",
|
||||||
|
"create_steam_shortcut": "Create Steam shortcut",
|
||||||
"create_shortcut_success": "Shortcut created successfully",
|
"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",
|
"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?",
|
"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",
|
"allow_nsfw_content": "Continue",
|
||||||
"refuse_nsfw_content": "Go back",
|
"refuse_nsfw_content": "Go back",
|
||||||
@@ -199,7 +201,10 @@
|
|||||||
"game_removed_from_favorites": "Game removed from favorites",
|
"game_removed_from_favorites": "Game removed from favorites",
|
||||||
"game_added_to_favorites": "Game added to favorites",
|
"game_added_to_favorites": "Game added to favorites",
|
||||||
"automatically_extract_downloaded_files": "Automatically extract downloaded files",
|
"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"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
@@ -358,7 +363,24 @@
|
|||||||
"install_common_redist": "Install",
|
"install_common_redist": "Install",
|
||||||
"installing_common_redist": "Installing…",
|
"installing_common_redist": "Installing…",
|
||||||
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
|
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
|
||||||
"extract_files_by_default": "Extract files by default after download"
|
"extract_files_by_default": "Extract files by default after download",
|
||||||
|
"achievement_custom_notification_position": "Achievement custom notification position",
|
||||||
|
"top-left": "Top left",
|
||||||
|
"top-center": "Top center",
|
||||||
|
"top-right": "Top right",
|
||||||
|
"bottom-left": "Bottom left",
|
||||||
|
"bottom-center": "Bottom center",
|
||||||
|
"bottom-right": "Bottom right",
|
||||||
|
"enable_achievement_custom_notifications": "Enable achievement custom notifications",
|
||||||
|
"alignment": "Alignment",
|
||||||
|
"variation": "Variation",
|
||||||
|
"default": "Default",
|
||||||
|
"rare": "Rare",
|
||||||
|
"platinum": "Platinum",
|
||||||
|
"hidden": "Hidden",
|
||||||
|
"test_notification": "Test notification",
|
||||||
|
"notification_preview": "Achievement Notification Preview",
|
||||||
|
"enable_friend_start_game_notifications": "When a friend starts playing a game"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
@@ -370,10 +392,13 @@
|
|||||||
"restart_to_install_update": "Restart Hydra to install the update",
|
"restart_to_install_update": "Restart Hydra to install the update",
|
||||||
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
|
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
|
||||||
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
|
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
|
||||||
"new_friend_request_description": "You have received a new friend request",
|
"new_friend_request_description": "{{displayName}} sent you a friend request",
|
||||||
"new_friend_request_title": "New friend request",
|
"new_friend_request_title": "New friend request",
|
||||||
"extraction_complete": "Extraction complete",
|
"extraction_complete": "Extraction complete",
|
||||||
"game_extracted": "{{title}} extracted successfully"
|
"game_extracted": "{{title}} extracted successfully",
|
||||||
|
"friend_started_playing_game": "{{displayName}} started playing a game",
|
||||||
|
"test_achievement_notification_title": "This is a test notification",
|
||||||
|
"test_achievement_notification_description": "Pretty cool, huh?"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Open Hydra",
|
"open": "Open Hydra",
|
||||||
|
|||||||
@@ -130,8 +130,10 @@
|
|||||||
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)",
|
"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_in_progress": "Descarga en progreso",
|
||||||
"download_paused": "Descarga pausada",
|
"download_paused": "Descarga pausada",
|
||||||
|
"create_steam_shortcut": "Crear atajo de Steam",
|
||||||
"last_downloaded_option": "Última opción descargada",
|
"last_downloaded_option": "Última opción descargada",
|
||||||
"create_shortcut_success": "Atajo creado con éxito",
|
"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",
|
"create_shortcut_error": "Error al crear un atajo",
|
||||||
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
"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?",
|
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
|
||||||
@@ -198,7 +200,10 @@
|
|||||||
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
"download_error_not_cached_on_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 el estado de descarga del sondeo aún no está disponible.",
|
||||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||||
"game_removed_from_favorites": "Juego removido de favoritos"
|
"game_removed_from_favorites": "Juego removido de favoritos",
|
||||||
|
"invalid_wine_prefix_path": "Ruta de prefixo Wine inválida",
|
||||||
|
"invalid_wine_prefix_path_description": "La ruta al prefixo Wine es inválida. Por favor, verifica la ruta y vuelve a intentarlo.",
|
||||||
|
"missing_wine_prefix": ""
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
@@ -371,7 +376,8 @@
|
|||||||
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
|
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
|
||||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
||||||
"new_friend_request_title": "Nueva solicitud de amistad",
|
"new_friend_request_title": "Nueva solicitud de amistad",
|
||||||
"new_friend_request_description": "Has recibido una nueva solicitud de amistad"
|
"new_friend_request_description": "{{displayName}} te envió una solicitud de amistad",
|
||||||
|
"friend_started_playing_game": "{{displayName}} está jugando"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Abrir Hydra",
|
"open": "Abrir Hydra",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import nb from "./nb/translation.json";
|
|||||||
import et from "./et/translation.json";
|
import et from "./et/translation.json";
|
||||||
import bg from "./bg/translation.json";
|
import bg from "./bg/translation.json";
|
||||||
import uz from "./uz/translation.json";
|
import uz from "./uz/translation.json";
|
||||||
|
import sv from "./sv/translation.json";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
"pt-BR": ptBR,
|
"pt-BR": ptBR,
|
||||||
@@ -56,4 +57,5 @@ export default {
|
|||||||
nb,
|
nb,
|
||||||
et,
|
et,
|
||||||
uz,
|
uz,
|
||||||
|
sv,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -118,7 +118,9 @@
|
|||||||
"download_in_progress": "Download em andamento",
|
"download_in_progress": "Download em andamento",
|
||||||
"download_paused": "Download pausado",
|
"download_paused": "Download pausado",
|
||||||
"last_downloaded_option": "Última opção baixada",
|
"last_downloaded_option": "Última opção baixada",
|
||||||
|
"create_steam_shortcut": "Criar atalho na Steam",
|
||||||
"create_shortcut_success": "Atalho criado com sucesso",
|
"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",
|
"create_shortcut_error": "Erro ao criar atalho",
|
||||||
"nsfw_content_title": "Este jogo contém conteúdo inapropriado",
|
"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?",
|
"nsfw_content_description": "{{title}} contém conteúdo que pode não ser apropriado para todas as idades. Você deseja continuar?",
|
||||||
@@ -188,7 +190,9 @@
|
|||||||
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||||
"game_added_to_favorites": "Jogo adicionado aos favoritos",
|
"game_added_to_favorites": "Jogo adicionado aos favoritos",
|
||||||
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados",
|
"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."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@@ -345,7 +349,24 @@
|
|||||||
"install_common_redist": "Instalar",
|
"install_common_redist": "Instalar",
|
||||||
"installing_common_redist": "Instalando…",
|
"installing_common_redist": "Instalando…",
|
||||||
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
|
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
|
||||||
"extract_files_by_default": "Extrair arquivos automaticamente após o download"
|
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
|
||||||
|
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
|
||||||
|
"top-left": "Superior esquerdo",
|
||||||
|
"top-center": "Superior central",
|
||||||
|
"top-right": "Superior direito",
|
||||||
|
"bottom-left": "Inferior esquerdo",
|
||||||
|
"bottom-right": "Inferior direito",
|
||||||
|
"bottom-center": "Inferior central",
|
||||||
|
"achievement_custom_notification_position": "Posição das notificações customizadas de conquista",
|
||||||
|
"alignment": "Alinhamento",
|
||||||
|
"variation": "Variação",
|
||||||
|
"default": "Padrão",
|
||||||
|
"rare": "Rara",
|
||||||
|
"platinum": "Platina",
|
||||||
|
"hidden": "Oculta",
|
||||||
|
"test_notification": "Testar notificação",
|
||||||
|
"notification_preview": "Prévia da Notificação de Conquistas",
|
||||||
|
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
@@ -356,9 +377,12 @@
|
|||||||
"new_update_available": "Versão {{version}} disponível",
|
"new_update_available": "Versão {{version}} disponível",
|
||||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
|
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
|
||||||
"new_friend_request_title": "Novo pedido de amizade",
|
"new_friend_request_title": "Novo pedido de amizade",
|
||||||
"new_friend_request_description": "Você recebeu um novo pedido de amizade",
|
"new_friend_request_description": "{{displayName}} te enviou um pedido de amizade",
|
||||||
"extraction_complete": "Extração concluída",
|
"extraction_complete": "Extração concluída",
|
||||||
"game_extracted": "{{title}} extraído com sucesso"
|
"game_extracted": "{{title}} extraído com sucesso",
|
||||||
|
"friend_started_playing_game": "{{displayName}} começou a jogar",
|
||||||
|
"test_achievement_notification_title": "Esta é uma notificação de teste",
|
||||||
|
"test_achievement_notification_description": "Bem legal, né?"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Abrir Hydra",
|
"open": "Abrir Hydra",
|
||||||
|
|||||||
@@ -341,7 +341,8 @@
|
|||||||
"new_update_available": "Versão {{version}} disponível",
|
"new_update_available": "Versão {{version}} disponível",
|
||||||
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
|
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
|
||||||
"new_friend_request_title": "Novo pedido de amizade",
|
"new_friend_request_title": "Novo pedido de amizade",
|
||||||
"new_friend_request_description": "Recebeste um novo pedido de amizade"
|
"new_friend_request_description": "{{displayName}} te enviou um pedido de amizade",
|
||||||
|
"friend_started_playing_game": "{{displayName}} começou a jogar"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Abrir o Hydra",
|
"open": "Abrir o Hydra",
|
||||||
|
|||||||
532
src/locales/sv/translation.json
Normal file
532
src/locales/sv/translation.json
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
{
|
||||||
|
"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": "Delar",
|
||||||
|
"stop_seeding": "Sluta dela",
|
||||||
|
"resume_seeding": "Fortsätt dela",
|
||||||
|
"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": "Dela 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"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ import { app } from "electron";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { SystemPath } from "./services/system-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 defaultDownloadsPath = SystemPath.getPath("downloads");
|
||||||
|
|
||||||
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
||||||
@@ -16,6 +14,8 @@ export const windowsStartMenuPath = path.join(
|
|||||||
"Programs"
|
"Programs"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const publicProfilePath = "C:/Users/Public";
|
||||||
|
|
||||||
export const levelDatabasePath = path.join(
|
export const levelDatabasePath = path.join(
|
||||||
SystemPath.getPath("userData"),
|
SystemPath.getPath("userData"),
|
||||||
`hydra-db${isStaging ? "-staging" : ""}`
|
`hydra-db${isStaging ? "-staging" : ""}`
|
||||||
@@ -40,4 +40,6 @@ export const backupsPath = path.join(SystemPath.getPath("userData"), "Backups");
|
|||||||
|
|
||||||
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
||||||
|
|
||||||
|
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
|
||||||
|
|
||||||
export const MAIN_LOOP_INTERVAL = 1500;
|
export const MAIN_LOOP_INTERVAL = 1500;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
import {
|
||||||
|
DownloadManager,
|
||||||
|
HydraApi,
|
||||||
|
WSClient,
|
||||||
|
gamesPlaytime,
|
||||||
|
} from "@main/services";
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
@@ -30,6 +35,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
databaseOperations,
|
databaseOperations,
|
||||||
HydraApi.post("/auth/logout").catch(() => {}),
|
HydraApi.post("/auth/logout").catch(() => {}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
WSClient.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("signOut", signOut);
|
registerEvent("signOut", signOut);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { CatalogueCategory } from "@shared";
|
import { CatalogueCategory } from "@shared";
|
||||||
|
import { ShopAssets } from "@types";
|
||||||
|
|
||||||
const getCatalogue = async (
|
const getCatalogue = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -11,7 +12,7 @@ const getCatalogue = async (
|
|||||||
skip: "0",
|
skip: "0",
|
||||||
});
|
});
|
||||||
|
|
||||||
return HydraApi.get(
|
return HydraApi.get<ShopAssets[]>(
|
||||||
`/catalogue/${category}?${params.toString()}`,
|
`/catalogue/${category}?${params.toString()}`,
|
||||||
{},
|
{},
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { getSteamAppDetails, logger } from "@main/services";
|
import { getSteamAppDetails, logger } from "@main/services";
|
||||||
|
|
||||||
import type { ShopDetails, GameShop } from "@types";
|
import type { ShopDetails, GameShop, ShopDetailsWithAssets } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import {
|
||||||
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
|
gamesShopAssetsSublevel,
|
||||||
|
gamesShopCacheSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
|
|
||||||
const getLocalizedSteamAppDetails = async (
|
const getLocalizedSteamAppDetails = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -14,22 +17,7 @@ const getLocalizedSteamAppDetails = async (
|
|||||||
return getSteamAppDetails(objectId, language);
|
return getSteamAppDetails(objectId, language);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSteamAppDetails(objectId, language).then(
|
return getSteamAppDetails(objectId, language);
|
||||||
async (localizedAppDetails) => {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (steamGame && localizedAppDetails) {
|
|
||||||
return {
|
|
||||||
...localizedAppDetails,
|
|
||||||
name: steamGame.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGameShopDetails = async (
|
const getGameShopDetails = async (
|
||||||
@@ -37,34 +25,44 @@ const getGameShopDetails = async (
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
language: string
|
language: string
|
||||||
): Promise<ShopDetails | null> => {
|
): Promise<ShopDetailsWithAssets | null> => {
|
||||||
if (shop === "steam") {
|
if (shop === "steam") {
|
||||||
const cachedData = await gamesShopCacheSublevel.get(
|
const [cachedData, cachedAssets] = await Promise.all([
|
||||||
levelKeys.gameShopCacheItem(shop, objectId, language)
|
gamesShopCacheSublevel.get(
|
||||||
);
|
levelKeys.gameShopCacheItem(shop, objectId, language)
|
||||||
|
),
|
||||||
|
gamesShopAssetsSublevel.get(levelKeys.game(shop, objectId)),
|
||||||
|
]);
|
||||||
|
|
||||||
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
result.name = cachedAssets?.title ?? result.name;
|
||||||
|
|
||||||
gamesShopCacheSublevel
|
gamesShopCacheSublevel
|
||||||
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error("Could not cache game details", err);
|
logger.error("Could not cache game details", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
assets: cachedAssets ?? null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
return {
|
return {
|
||||||
...cachedData,
|
...cachedData,
|
||||||
objectId,
|
assets: cachedAssets ?? null,
|
||||||
} as ShopDetails;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(appDetails);
|
return appDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import type { TrendingGame } from "@types";
|
|||||||
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const language = await db
|
const language = await db
|
||||||
.get<string, string>(levelKeys.language, {
|
.get<string, string>(levelKeys.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf8",
|
||||||
})
|
})
|
||||||
.then((language) => language || "en");
|
.then((language) => language || "en");
|
||||||
|
|
||||||
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
||||||
"/games/trending",
|
"/games/featured",
|
||||||
{ language },
|
{ language },
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
).catch(() => []);
|
).catch(() => []);
|
||||||
|
|
||||||
return trendingGames;
|
return trendingGames.slice(0, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getTrendingGames", getTrendingGames);
|
registerEvent("getTrendingGames", getTrendingGames);
|
||||||
|
|||||||
14
src/main/events/catalogue/save-game-shop-assets.ts
Normal file
14
src/main/events/catalogue/save-game-shop-assets.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { GameShop, ShopAssets } from "@types";
|
||||||
|
import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const saveGameShopAssets = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
assets: ShopAssets
|
||||||
|
): Promise<void> => {
|
||||||
|
return gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), 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 fs from "node:fs";
|
||||||
import * as tar from "tar";
|
import * as tar from "tar";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { backupsPath } from "@main/constants";
|
import { backupsPath, publicProfilePath } from "@main/constants";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop, LudusaviBackupMapping } from "@types";
|
||||||
|
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
import { normalizePath } from "@main/helpers";
|
import { addTrailingSlash, normalizePath } from "@main/helpers";
|
||||||
import { SystemPath } from "@main/services/system-path";
|
import { SystemPath } from "@main/services/system-path";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
export interface LudusaviBackup {
|
export const transformLudusaviBackupPathIntoWindowsPath = (
|
||||||
files: {
|
backupPath: string,
|
||||||
[key: string]: {
|
winePrefixPath?: string | null
|
||||||
hash: string;
|
) => {
|
||||||
size: number;
|
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,
|
backupPath: string,
|
||||||
title: string,
|
title: string,
|
||||||
homeDir: string
|
homeDir: string,
|
||||||
|
winePrefixPath?: string | null,
|
||||||
|
artifactWinePrefixPath?: string | null
|
||||||
) => {
|
) => {
|
||||||
const gameBackupPath = path.join(backupPath, title);
|
const gameBackupPath = path.join(backupPath, title);
|
||||||
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
|
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
|
||||||
|
|
||||||
const data = fs.readFileSync(mappingYamlPath, "utf8");
|
const data = fs.readFileSync(mappingYamlPath, "utf8");
|
||||||
const manifest = YAML.parse(data) as {
|
const manifest = YAML.parse(data) as {
|
||||||
backups: LudusaviBackup[];
|
backups: LudusaviBackupMapping[];
|
||||||
drives: Record<string, string>;
|
drives: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentHomeDir = normalizePath(SystemPath.getPath("home"));
|
const userProfilePath =
|
||||||
|
CloudSync.getWindowsLikeUserProfilePath(winePrefixPath);
|
||||||
|
|
||||||
/* Renaming logic */
|
manifest.backups.forEach((backup) => {
|
||||||
if (os.platform() === "win32") {
|
Object.keys(backup.files).forEach((key) => {
|
||||||
const mappedHomeDir = path.join(
|
const sourcePathWithDrives = Object.entries(manifest.drives).reduce(
|
||||||
gameBackupPath,
|
(prev, [driveKey, driveValue]) => {
|
||||||
path.join("drive-C", homeDir.replace("C:", ""))
|
return prev.replace(driveValue, driveKey);
|
||||||
);
|
},
|
||||||
|
key
|
||||||
if (fs.existsSync(mappedHomeDir)) {
|
|
||||||
fs.renameSync(
|
|
||||||
mappedHomeDir,
|
|
||||||
path.join(gameBackupPath, "drive-C", currentHomeDir.replace("C:", ""))
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const backups = manifest.backups.map((backup: LudusaviBackup) => {
|
const sourcePath = path.join(gameBackupPath, sourcePathWithDrives);
|
||||||
const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
|
|
||||||
const updatedKey = key.replace(homeDir, currentHomeDir);
|
|
||||||
|
|
||||||
return {
|
logger.info(`Source path: ${sourcePath}`);
|
||||||
...prev,
|
|
||||||
[updatedKey]: value,
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
const destinationPath = transformLudusaviBackupPathIntoWindowsPath(
|
||||||
...backup,
|
key,
|
||||||
files,
|
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 (
|
const downloadGameArtifact = async (
|
||||||
@@ -78,10 +97,18 @@ const downloadGameArtifact = async (
|
|||||||
gameArtifactId: string
|
gameArtifactId: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
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;
|
downloadUrl: string;
|
||||||
objectKey: string;
|
objectKey: string;
|
||||||
homeDir: string;
|
homeDir: string;
|
||||||
|
winePrefixPath: string | null;
|
||||||
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
|
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
|
||||||
|
|
||||||
const zipLocation = path.join(SystemPath.getPath("userData"), objectKey);
|
const zipLocation = path.join(SystemPath.getPath("userData"), objectKey);
|
||||||
@@ -109,34 +136,34 @@ const downloadGameArtifact = async (
|
|||||||
response.data.pipe(writer);
|
response.data.pipe(writer);
|
||||||
|
|
||||||
writer.on("error", (err) => {
|
writer.on("error", (err) => {
|
||||||
logger.error("Failed to write zip", err);
|
logger.error("Failed to write tar file", err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.mkdirSync(backupPath, { recursive: true });
|
fs.mkdirSync(backupPath, { recursive: true });
|
||||||
|
|
||||||
writer.on("close", () => {
|
writer.on("close", async () => {
|
||||||
tar
|
await tar.x({
|
||||||
.x({
|
file: zipLocation,
|
||||||
file: zipLocation,
|
cwd: backupPath,
|
||||||
cwd: backupPath,
|
});
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
replaceLudusaviBackupWithCurrentUser(
|
|
||||||
backupPath,
|
|
||||||
objectId,
|
|
||||||
normalizePath(homeDir)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ludusavi.restoreBackup(backupPath).then(() => {
|
restoreLudusaviBackup(
|
||||||
WindowManager.mainWindow?.webContents.send(
|
backupPath,
|
||||||
`on-backup-download-complete-${objectId}-${shop}`,
|
objectId,
|
||||||
true
|
normalizePath(homeDir),
|
||||||
);
|
game?.winePrefixPath,
|
||||||
});
|
artifactWinePrefixPath
|
||||||
});
|
);
|
||||||
|
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-backup-download-complete-${objectId}-${shop}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
logger.error("Failed to download game artifact", err);
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
`on-backup-download-complete-${objectId}-${shop}`,
|
`on-backup-download-complete-${objectId}-${shop}`,
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ipcMain } from "electron";
|
|||||||
|
|
||||||
import "./catalogue/get-catalogue";
|
import "./catalogue/get-catalogue";
|
||||||
import "./catalogue/get-game-shop-details";
|
import "./catalogue/get-game-shop-details";
|
||||||
|
import "./catalogue/save-game-shop-assets";
|
||||||
import "./catalogue/get-how-long-to-beat";
|
import "./catalogue/get-how-long-to-beat";
|
||||||
import "./catalogue/get-random-game";
|
import "./catalogue/get-random-game";
|
||||||
import "./catalogue/search-games";
|
import "./catalogue/search-games";
|
||||||
@@ -33,6 +34,8 @@ import "./library/remove-game-from-library";
|
|||||||
import "./library/select-game-wine-prefix";
|
import "./library/select-game-wine-prefix";
|
||||||
import "./library/reset-game-achievements";
|
import "./library/reset-game-achievements";
|
||||||
import "./library/toggle-automatic-cloud-sync";
|
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-checkout";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
@@ -84,6 +87,8 @@ import "./cloud-save/upload-save-game";
|
|||||||
import "./cloud-save/delete-game-artifact";
|
import "./cloud-save/delete-game-artifact";
|
||||||
import "./cloud-save/select-game-backup-path";
|
import "./cloud-save/select-game-backup-path";
|
||||||
import "./notifications/publish-new-repacks-notification";
|
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/add-custom-theme";
|
||||||
import "./themes/delete-custom-theme";
|
import "./themes/delete-custom-theme";
|
||||||
import "./themes/get-all-custom-themes";
|
import "./themes/get-all-custom-themes";
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import {
|
||||||
|
downloadsSublevel,
|
||||||
|
gamesShopAssetsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -17,25 +18,18 @@ const addGameToLibrary = async (
|
|||||||
const gameKey = levelKeys.game(shop, objectId);
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
let game = await gamesSublevel.get(gameKey);
|
let game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
await downloadsSublevel.del(gameKey);
|
await downloadsSublevel.del(gameKey);
|
||||||
|
|
||||||
await gamesSublevel.put(gameKey, {
|
game.isDeleted = false;
|
||||||
...game,
|
|
||||||
isDeleted: false,
|
await gamesSublevel.put(gameKey, game);
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
game = {
|
game = {
|
||||||
title,
|
title,
|
||||||
iconUrl,
|
iconUrl: gameAssets?.iconUrl ?? null,
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
remoteId: null,
|
remoteId: null,
|
||||||
@@ -44,7 +38,7 @@ const addGameToLibrary = async (
|
|||||||
lastTimePlayed: null,
|
lastTimePlayed: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
await gamesSublevel.put(gameKey, game);
|
||||||
}
|
}
|
||||||
|
|
||||||
await createGame(game).catch(() => {});
|
await createGame(game).catch(() => {});
|
||||||
|
|||||||
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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =
|
||||||
|
await downloadAssetsFromSteam(game.shop, game.objectId, assets);
|
||||||
|
|
||||||
|
const newShortcut = composeSteamShortcut(
|
||||||
|
game.title,
|
||||||
|
game.executablePath,
|
||||||
|
iconImage
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const steamUserId of steamUserIds) {
|
||||||
|
logger.info("Adding shortcut for Steam user", steamUserId);
|
||||||
|
|
||||||
|
const steamShortcuts = await getSteamShortcuts(steamUserId);
|
||||||
|
|
||||||
|
if (steamShortcuts.some((shortcut) => shortcut.appname === game.title)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridPath = path.join(
|
||||||
|
await getSteamLocation(),
|
||||||
|
"userdata",
|
||||||
|
steamUserId.toString(),
|
||||||
|
"config",
|
||||||
|
"grid"
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.promises.mkdir(gridPath, { recursive: true });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
copyAssetIfExists(
|
||||||
|
heroImage,
|
||||||
|
path.join(gridPath, `${newShortcut.appid}_hero.jpg`)
|
||||||
|
),
|
||||||
|
copyAssetIfExists(
|
||||||
|
logoImage,
|
||||||
|
path.join(gridPath, `${newShortcut.appid}_logo.png`)
|
||||||
|
),
|
||||||
|
copyAssetIfExists(
|
||||||
|
coverImage,
|
||||||
|
path.join(gridPath, `${newShortcut.appid}p.jpg`)
|
||||||
|
),
|
||||||
|
copyAssetIfExists(
|
||||||
|
libraryImage,
|
||||||
|
path.join(gridPath, `${newShortcut.appid}.jpg`)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
steamShortcuts.push(newShortcut);
|
||||||
|
|
||||||
|
logger.info(newShortcut);
|
||||||
|
logger.info("Writing Steam shortcuts", steamShortcuts);
|
||||||
|
|
||||||
|
await writeSteamShortcuts(steamUserId, steamShortcuts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "linux" && !game.winePrefixPath) {
|
||||||
|
const steamWinePrefixes = path.join(
|
||||||
|
SystemPath.getPath("home"),
|
||||||
|
".local",
|
||||||
|
"share",
|
||||||
|
"Steam",
|
||||||
|
"steamapps",
|
||||||
|
"compatdata"
|
||||||
|
);
|
||||||
|
|
||||||
|
const winePrefixPath = path.join(
|
||||||
|
steamWinePrefixes,
|
||||||
|
newShortcut.appid.toString(),
|
||||||
|
"pfx"
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.promises.mkdir(winePrefixPath, { recursive: true });
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
winePrefixPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("createSteamShortcut", createSteamShortcut);
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { downloadsSublevel, gamesSublevel } from "@main/level";
|
import {
|
||||||
|
downloadsSublevel,
|
||||||
|
gamesShopAssetsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
} from "@main/level";
|
||||||
|
|
||||||
const getLibrary = async (): Promise<LibraryGame[]> => {
|
const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||||
return gamesSublevel
|
return gamesSublevel
|
||||||
@@ -12,11 +16,13 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
|
|||||||
.filter(([_key, game]) => game.isDeleted === false)
|
.filter(([_key, game]) => game.isDeleted === false)
|
||||||
.map(async ([key, game]) => {
|
.map(async ([key, game]) => {
|
||||||
const download = await downloadsSublevel.get(key);
|
const download = await downloadsSublevel.get(key);
|
||||||
|
const gameAssets = await gamesShopAssetsSublevel.get(key);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: key,
|
id: key,
|
||||||
...game,
|
...game,
|
||||||
download: download ?? null,
|
download: download ?? null,
|
||||||
|
...gameAssets,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,16 +12,14 @@ const openGameInstallerPath = async (
|
|||||||
) => {
|
) => {
|
||||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
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(
|
const gamePath = path.join(
|
||||||
download.downloadPath ?? (await getDownloadsPath()),
|
download.downloadPath ?? (await getDownloadsPath()),
|
||||||
download.folderName!
|
download.folderName
|
||||||
);
|
);
|
||||||
|
|
||||||
shell.showItemInFolder(gamePath);
|
shell.showItemInFolder(gamePath);
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("openGameInstallerPath", openGameInstallerPath);
|
registerEvent("openGameInstallerPath", openGameInstallerPath);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import fs from "node:fs";
|
||||||
import { levelKeys, gamesSublevel } from "@main/level";
|
import { levelKeys, gamesSublevel } from "@main/level";
|
||||||
|
import { Wine } from "@main/services";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const selectGameWinePrefix = async (
|
const selectGameWinePrefix = async (
|
||||||
@@ -14,9 +16,24 @@ const selectGameWinePrefix = async (
|
|||||||
|
|
||||||
if (!game) return;
|
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, {
|
await gamesSublevel.put(gameKey, {
|
||||||
...game,
|
...game,
|
||||||
winePrefixPath: winePrefixPath,
|
winePrefixPath: realWinePrefixPath,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ const verifyExecutablePathInUse = async (
|
|||||||
) => {
|
) => {
|
||||||
for await (const game of gamesSublevel.values()) {
|
for await (const game of gamesSublevel.values()) {
|
||||||
if (game.executablePath === executablePath) {
|
if (game.executablePath === executablePath) {
|
||||||
return true;
|
return game;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { db, levelKeys } from "@main/level";
|
|||||||
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const language = await db
|
const language = await db
|
||||||
.get<string, string>(levelKeys.language, {
|
.get<string, string>(levelKeys.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf8",
|
||||||
})
|
})
|
||||||
.then((language) => language || "en");
|
.then((language) => language || "en");
|
||||||
|
|
||||||
|
|||||||
@@ -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,34 +1,11 @@
|
|||||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi, WindowManager } from "@main/services";
|
import { HydraApi, WindowManager } from "@main/services";
|
||||||
import { publishNewFriendRequestNotification } from "@main/services/notifications";
|
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import type { FriendRequestSync } from "@types";
|
import type { FriendRequestSync } from "@types";
|
||||||
|
|
||||||
interface SyncState {
|
export const syncFriendRequests = async () => {
|
||||||
friendRequestCount: number | null;
|
|
||||||
tick: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ticksToUpdate = (2 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 2 minutes
|
|
||||||
|
|
||||||
const syncState: SyncState = {
|
|
||||||
friendRequestCount: null,
|
|
||||||
tick: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncFriendRequests = async () => {
|
|
||||||
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`)
|
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (
|
|
||||||
syncState.friendRequestCount != null &&
|
|
||||||
syncState.friendRequestCount < res.friendRequestCount
|
|
||||||
) {
|
|
||||||
publishNewFriendRequestNotification();
|
|
||||||
}
|
|
||||||
|
|
||||||
syncState.friendRequestCount = res.friendRequestCount;
|
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
"on-sync-friend-requests",
|
"on-sync-friend-requests",
|
||||||
res
|
res
|
||||||
@@ -44,16 +21,4 @@ const syncFriendRequests = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncFriendRequestsEvent = async (_event: Electron.IpcMainInvokeEvent) => {
|
registerEvent("syncFriendRequests", syncFriendRequests);
|
||||||
return syncFriendRequests();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const watchFriendRequests = async () => {
|
|
||||||
if (syncState.tick % ticksToUpdate === 0) {
|
|
||||||
await syncFriendRequests();
|
|
||||||
}
|
|
||||||
|
|
||||||
syncState.tick++;
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("syncFriendRequests", syncFriendRequestsEvent);
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { themesSublevel } from "@main/level";
|
import { themesSublevel } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
|
||||||
const toggleCustomTheme = async (
|
const toggleCustomTheme = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -17,6 +18,8 @@ const toggleCustomTheme = async (
|
|||||||
isActive,
|
isActive,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
WindowManager.notificationWindow?.webContents.send("on-custom-theme-updated");
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("toggleCustomTheme", toggleCustomTheme);
|
registerEvent("toggleCustomTheme", toggleCustomTheme);
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ const updateCustomTheme = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (theme.isActive) {
|
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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { Download, StartGameDownloadPayload } from "@types";
|
import type { Download, StartGameDownloadPayload } from "@types";
|
||||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { Downloader, DownloadError, steamUrlBuilder } from "@shared";
|
import { Downloader, DownloadError } from "@shared";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import {
|
||||||
|
downloadsSublevel,
|
||||||
|
gamesShopAssetsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
@@ -36,6 +39,7 @@ const startGameDownload = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const game = await gamesSublevel.get(gameKey);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
|
||||||
/* Delete any previous download */
|
/* Delete any previous download */
|
||||||
await downloadsSublevel.del(gameKey);
|
await downloadsSublevel.del(gameKey);
|
||||||
@@ -46,17 +50,9 @@ const startGameDownload = async (
|
|||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
await gamesSublevel.put(gameKey, {
|
await gamesSublevel.put(gameKey, {
|
||||||
title,
|
title,
|
||||||
iconUrl,
|
iconUrl: gameAssets?.iconUrl ?? null,
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
remoteId: null,
|
remoteId: null,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const updateUserPreferences = async (
|
|||||||
|
|
||||||
if (preferences.language) {
|
if (preferences.language) {
|
||||||
await db.put<string, string>(levelKeys.language, preferences.language, {
|
await db.put<string, string>(levelKeys.language, preferences.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf8",
|
||||||
});
|
});
|
||||||
|
|
||||||
i18next.changeLanguage(preferences.language);
|
i18next.changeLanguage(preferences.language);
|
||||||
|
|||||||
@@ -1,97 +1,12 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi, logger } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
import type { UserProfile } from "@types";
|
import type { UserProfile } from "@types";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
|
|
||||||
const getSteamGame = async (objectId: string) => {
|
|
||||||
try {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: steamGame.name as string,
|
|
||||||
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to get Steam game", err);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUser = async (
|
const getUser = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<UserProfile | null> => {
|
): Promise<UserProfile | null> => {
|
||||||
try {
|
return HydraApi.get<UserProfile>(`/users/${userId}`).catch(() => null);
|
||||||
const profile = await HydraApi.get<UserProfile | null>(`/users/${userId}`);
|
|
||||||
|
|
||||||
if (!profile) return null;
|
|
||||||
|
|
||||||
const recentGames = await Promise.all(
|
|
||||||
profile.recentGames
|
|
||||||
.map(async (game) => {
|
|
||||||
const steamGame = await getSteamGame(game.objectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...game,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((game) => game)
|
|
||||||
);
|
|
||||||
|
|
||||||
const libraryGames = await Promise.all(
|
|
||||||
profile.libraryGames
|
|
||||||
.map(async (game) => {
|
|
||||||
const steamGame = await getSteamGame(game.objectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...game,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((game) => game)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (profile.currentGame) {
|
|
||||||
const steamGame = await getSteamGame(profile.currentGame.objectId);
|
|
||||||
|
|
||||||
if (steamGame) {
|
|
||||||
profile.currentGame = {
|
|
||||||
...profile.currentGame,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const friends = await Promise.all(
|
|
||||||
profile.friends.map(async (friend) => {
|
|
||||||
if (!friend.currentGame) return friend;
|
|
||||||
|
|
||||||
const currentGame = await getSteamGame(friend.currentGame.objectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...friend,
|
|
||||||
currentGame: {
|
|
||||||
...friend.currentGame,
|
|
||||||
...currentGame,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...profile,
|
|
||||||
friends,
|
|
||||||
libraryGames,
|
|
||||||
recentGames,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getUser", getUser);
|
registerEvent("getUser", getUser);
|
||||||
|
|||||||
352
src/main/generated/envelope.ts
Normal file
352
src/main/generated/envelope.ts
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
// @generated by protobuf-ts 2.10.0
|
||||||
|
// @generated from protobuf file "envelope.proto" (syntax proto3)
|
||||||
|
// tslint:disable
|
||||||
|
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryWriter } from "@protobuf-ts/runtime";
|
||||||
|
import { WireType } from "@protobuf-ts/runtime";
|
||||||
|
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryReader } from "@protobuf-ts/runtime";
|
||||||
|
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
|
||||||
|
import type { PartialMessage } from "@protobuf-ts/runtime";
|
||||||
|
import { reflectionMergePartial } from "@protobuf-ts/runtime";
|
||||||
|
import { MessageType } from "@protobuf-ts/runtime";
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message FriendRequest
|
||||||
|
*/
|
||||||
|
export interface FriendRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: int32 friend_request_count = 1;
|
||||||
|
*/
|
||||||
|
friendRequestCount: number;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: optional string sender_id = 2;
|
||||||
|
*/
|
||||||
|
senderId?: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message FriendGameSession
|
||||||
|
*/
|
||||||
|
export interface FriendGameSession {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string object_id = 1;
|
||||||
|
*/
|
||||||
|
objectId: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string shop = 2;
|
||||||
|
*/
|
||||||
|
shop: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string friend_id = 3;
|
||||||
|
*/
|
||||||
|
friendId: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message Envelope
|
||||||
|
*/
|
||||||
|
export interface Envelope {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf oneof: payload
|
||||||
|
*/
|
||||||
|
payload:
|
||||||
|
| {
|
||||||
|
oneofKind: "friendRequest";
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: FriendRequest friend_request = 1;
|
||||||
|
*/
|
||||||
|
friendRequest: FriendRequest;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
oneofKind: "friendGameSession";
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: FriendGameSession friend_game_session = 2;
|
||||||
|
*/
|
||||||
|
friendGameSession: FriendGameSession;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
oneofKind: undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class FriendRequest$Type extends MessageType<FriendRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("FriendRequest", [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
name: "friend_request_count",
|
||||||
|
kind: "scalar",
|
||||||
|
T: 5 /*ScalarType.INT32*/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
name: "sender_id",
|
||||||
|
kind: "scalar",
|
||||||
|
opt: true,
|
||||||
|
T: 9 /*ScalarType.STRING*/,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<FriendRequest>): FriendRequest {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.friendRequestCount = 0;
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<FriendRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: FriendRequest
|
||||||
|
): FriendRequest {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* int32 friend_request_count */ 1:
|
||||||
|
message.friendRequestCount = reader.int32();
|
||||||
|
break;
|
||||||
|
case /* optional string sender_id */ 2:
|
||||||
|
message.senderId = reader.string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: FriendRequest,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* int32 friend_request_count = 1; */
|
||||||
|
if (message.friendRequestCount !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).int32(message.friendRequestCount);
|
||||||
|
/* optional string sender_id = 2; */
|
||||||
|
if (message.senderId !== undefined)
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.senderId);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
writer
|
||||||
|
);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message FriendRequest
|
||||||
|
*/
|
||||||
|
export const FriendRequest = new FriendRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class FriendGameSession$Type extends MessageType<FriendGameSession> {
|
||||||
|
constructor() {
|
||||||
|
super("FriendGameSession", [
|
||||||
|
{ no: 1, name: "object_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 2, name: "shop", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 3, name: "friend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<FriendGameSession>): FriendGameSession {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.objectId = "";
|
||||||
|
message.shop = "";
|
||||||
|
message.friendId = "";
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<FriendGameSession>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: FriendGameSession
|
||||||
|
): FriendGameSession {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* string object_id */ 1:
|
||||||
|
message.objectId = reader.string();
|
||||||
|
break;
|
||||||
|
case /* string shop */ 2:
|
||||||
|
message.shop = reader.string();
|
||||||
|
break;
|
||||||
|
case /* string friend_id */ 3:
|
||||||
|
message.friendId = reader.string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: FriendGameSession,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* string object_id = 1; */
|
||||||
|
if (message.objectId !== "")
|
||||||
|
writer.tag(1, WireType.LengthDelimited).string(message.objectId);
|
||||||
|
/* string shop = 2; */
|
||||||
|
if (message.shop !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.shop);
|
||||||
|
/* string friend_id = 3; */
|
||||||
|
if (message.friendId !== "")
|
||||||
|
writer.tag(3, WireType.LengthDelimited).string(message.friendId);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
writer
|
||||||
|
);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message FriendGameSession
|
||||||
|
*/
|
||||||
|
export const FriendGameSession = new FriendGameSession$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class Envelope$Type extends MessageType<Envelope> {
|
||||||
|
constructor() {
|
||||||
|
super("Envelope", [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
name: "friend_request",
|
||||||
|
kind: "message",
|
||||||
|
oneof: "payload",
|
||||||
|
T: () => FriendRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
name: "friend_game_session",
|
||||||
|
kind: "message",
|
||||||
|
oneof: "payload",
|
||||||
|
T: () => FriendGameSession,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<Envelope>): Envelope {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.payload = { oneofKind: undefined };
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Envelope>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: Envelope
|
||||||
|
): Envelope {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* FriendRequest friend_request */ 1:
|
||||||
|
message.payload = {
|
||||||
|
oneofKind: "friendRequest",
|
||||||
|
friendRequest: FriendRequest.internalBinaryRead(
|
||||||
|
reader,
|
||||||
|
reader.uint32(),
|
||||||
|
options,
|
||||||
|
(message.payload as any).friendRequest
|
||||||
|
),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case /* FriendGameSession friend_game_session */ 2:
|
||||||
|
message.payload = {
|
||||||
|
oneofKind: "friendGameSession",
|
||||||
|
friendGameSession: FriendGameSession.internalBinaryRead(
|
||||||
|
reader,
|
||||||
|
reader.uint32(),
|
||||||
|
options,
|
||||||
|
(message.payload as any).friendGameSession
|
||||||
|
),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: Envelope,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* FriendRequest friend_request = 1; */
|
||||||
|
if (message.payload.oneofKind === "friendRequest")
|
||||||
|
FriendRequest.internalBinaryWrite(
|
||||||
|
message.payload.friendRequest,
|
||||||
|
writer.tag(1, WireType.LengthDelimited).fork(),
|
||||||
|
options
|
||||||
|
).join();
|
||||||
|
/* FriendGameSession friend_game_session = 2; */
|
||||||
|
if (message.payload.oneofKind === "friendGameSession")
|
||||||
|
FriendGameSession.internalBinaryWrite(
|
||||||
|
message.payload.friendGameSession,
|
||||||
|
writer.tag(2, WireType.LengthDelimited).fork(),
|
||||||
|
options
|
||||||
|
).join();
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
writer
|
||||||
|
);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message Envelope
|
||||||
|
*/
|
||||||
|
export const Envelope = new Envelope$Type();
|
||||||
@@ -32,3 +32,8 @@ export const isPortableVersion = () => {
|
|||||||
|
|
||||||
export const normalizePath = (str: string) =>
|
export const normalizePath = (str: string) =>
|
||||||
path.posix.normalize(str).replace(/\\/g, "/");
|
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;
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ autoUpdater.logger = logger;
|
|||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) app.quit();
|
if (!gotTheLock) app.quit();
|
||||||
|
|
||||||
app.commandLine.appendSwitch("--no-sandbox");
|
if (process.platform !== "linux") {
|
||||||
|
app.commandLine.appendSwitch("--no-sandbox");
|
||||||
|
}
|
||||||
|
|
||||||
i18n.init({
|
i18n.init({
|
||||||
resources,
|
resources,
|
||||||
@@ -59,9 +61,11 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
await loadState();
|
await loadState();
|
||||||
|
|
||||||
const language = await db.get<string, string>(levelKeys.language, {
|
const language = await db
|
||||||
valueEncoding: "utf-8",
|
.get<string, string>(levelKeys.language, {
|
||||||
});
|
valueEncoding: "utf8",
|
||||||
|
})
|
||||||
|
.catch(() => "en");
|
||||||
|
|
||||||
if (language) i18n.changeLanguage(language);
|
if (language) i18n.changeLanguage(language);
|
||||||
|
|
||||||
@@ -69,6 +73,7 @@ app.whenReady().then(async () => {
|
|||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WindowManager.createNotificationWindow();
|
||||||
WindowManager.createSystemTray(language || "en");
|
WindowManager.createSystemTray(language || "en");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
11
src/main/level/sublevels/game-shop-assets.ts
Normal file
11
src/main/level/sublevels/game-shop-assets.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { ShopAssets } from "@types";
|
||||||
|
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const gamesShopAssetsSublevel = db.sublevel<string, ShopAssets>(
|
||||||
|
levelKeys.gameShopAssets,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./downloads";
|
export * from "./downloads";
|
||||||
export * from "./games";
|
export * from "./games";
|
||||||
|
export * from "./game-shop-assets";
|
||||||
export * from "./game-shop-cache";
|
export * from "./game-shop-cache";
|
||||||
export * from "./game-achievements";
|
export * from "./game-achievements";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const levelKeys = {
|
|||||||
user: "user",
|
user: "user",
|
||||||
auth: "auth",
|
auth: "auth",
|
||||||
themes: "themes",
|
themes: "themes",
|
||||||
|
gameShopAssets: "gameShopAssets",
|
||||||
gameShopCache: "gameShopCache",
|
gameShopCache: "gameShopCache",
|
||||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||||
`${shop}:${objectId}:${language}`,
|
`${shop}:${objectId}:${language}`,
|
||||||
@@ -14,4 +15,5 @@ export const levelKeys = {
|
|||||||
userPreferences: "userPreferences",
|
userPreferences: "userPreferences",
|
||||||
language: "language",
|
language: "language",
|
||||||
screenState: "screenState",
|
screenState: "screenState",
|
||||||
|
rpcPassword: "rpcPassword",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { Aria2, DownloadManager, Ludusavi, startMainLoop } from "./services";
|
|
||||||
import { RealDebridClient } from "./services/download/real-debrid";
|
|
||||||
import { HydraApi } from "./services/hydra-api";
|
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
|
||||||
import { downloadsSublevel } from "./level/sublevels/downloads";
|
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { levelKeys, db } from "./level";
|
import { levelKeys, db } from "./level";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import { TorBoxClient } from "./services/download/torbox";
|
import {
|
||||||
import { CommonRedistManager } from "./services/common-redist-manager";
|
WSClient,
|
||||||
import { SystemPath } from "./services/system-path";
|
SystemPath,
|
||||||
|
CommonRedistManager,
|
||||||
|
TorBoxClient,
|
||||||
|
RealDebridClient,
|
||||||
|
Aria2,
|
||||||
|
DownloadManager,
|
||||||
|
HydraApi,
|
||||||
|
uploadGamesBatch,
|
||||||
|
startMainLoop,
|
||||||
|
Ludusavi,
|
||||||
|
} from "@main/services";
|
||||||
|
|
||||||
export const loadState = async () => {
|
export const loadState = async () => {
|
||||||
SystemPath.checkIfPathsAreAvailable();
|
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
@@ -23,7 +27,9 @@ export const loadState = async () => {
|
|||||||
|
|
||||||
await import("./events");
|
await import("./events");
|
||||||
|
|
||||||
Aria2.spawn();
|
if (process.platform !== "darwin") {
|
||||||
|
Aria2.spawn();
|
||||||
|
}
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
@@ -33,10 +39,11 @@ export const loadState = async () => {
|
|||||||
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.copyConfigFileToUserData();
|
||||||
|
|
||||||
await HydraApi.setupApi().then(() => {
|
await HydraApi.setupApi().then(() => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
|
WSClient.connect();
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloads = await downloadsSublevel
|
const downloads = await downloadsSublevel
|
||||||
@@ -70,4 +77,6 @@ export const loadState = async () => {
|
|||||||
startMainLoop();
|
startMainLoop();
|
||||||
|
|
||||||
CommonRedistManager.downloadCommonRedist();
|
CommonRedistManager.downloadCommonRedist();
|
||||||
|
|
||||||
|
SystemPath.checkIfPathsAreAvailable();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,18 @@ import {
|
|||||||
findAllAchievementFiles,
|
findAllAchievementFiles,
|
||||||
getAlternativeObjectIds,
|
getAlternativeObjectIds,
|
||||||
} from "./find-achivement-files";
|
} from "./find-achivement-files";
|
||||||
import type { AchievementFile, Game, UnlockedAchievement } from "@types";
|
import type {
|
||||||
|
AchievementFile,
|
||||||
|
Game,
|
||||||
|
UnlockedAchievement,
|
||||||
|
UserPreferences,
|
||||||
|
} from "@types";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||||
import { gamesSublevel } from "@main/level";
|
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { WindowManager } from "../window-manager";
|
||||||
|
import { sleep } from "@main/helpers";
|
||||||
|
|
||||||
const fileStats: Map<string, number> = new Map();
|
const fileStats: Map<string, number> = new Map();
|
||||||
const fltFiles: Map<string, Set<string>> = new Map();
|
const fltFiles: Map<string, Set<string>> = new Map();
|
||||||
@@ -184,7 +191,7 @@ export class AchievementWatcherManager {
|
|||||||
return mergeAchievements(game, unlockedAchievements, false);
|
return mergeAchievements(game, unlockedAchievements, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static preSearchAchievementsWindows = async () => {
|
private static async getGameAchievementFilesWindows() {
|
||||||
const games = await gamesSublevel
|
const games = await gamesSublevel
|
||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
@@ -194,24 +201,24 @@ export class AchievementWatcherManager {
|
|||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
games.map((game) => {
|
games.map((game) => {
|
||||||
const gameAchievementFiles: AchievementFile[] = [];
|
const achievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||||
gameAchievementFiles.push(
|
achievementFiles.push(
|
||||||
...(gameAchievementFilesMap.get(objectId) || [])
|
...(gameAchievementFilesMap.get(objectId) || [])
|
||||||
);
|
);
|
||||||
|
|
||||||
gameAchievementFiles.push(
|
achievementFiles.push(
|
||||||
...findAchievementFileInExecutableDirectory(game)
|
...findAchievementFileInExecutableDirectory(game)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
return { game, achievementFiles };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
private static preSearchAchievementsWithWine = async () => {
|
private static async getGameAchievementFilesLinux() {
|
||||||
const games = await gamesSublevel
|
const games = await gamesSublevel
|
||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
@@ -219,37 +226,70 @@ export class AchievementWatcherManager {
|
|||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
games.map((game) => {
|
games.map((game) => {
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const achievementFiles = findAchievementFiles(game);
|
||||||
const achievementFileInsideDirectory =
|
const achievementFileInsideDirectory =
|
||||||
findAchievementFileInExecutableDirectory(game);
|
findAchievementFileInExecutableDirectory(game);
|
||||||
|
|
||||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
achievementFiles.push(...achievementFileInsideDirectory);
|
||||||
|
|
||||||
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
return { game, achievementFiles };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
public static async preSearchAchievements() {
|
public static async preSearchAchievements() {
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newAchievementsCount =
|
const gameAchievementFiles =
|
||||||
process.platform === "win32"
|
process.platform === "win32"
|
||||||
? await this.preSearchAchievementsWindows()
|
? await this.getGameAchievementFilesWindows()
|
||||||
: await this.preSearchAchievementsWithWine();
|
: await this.getGameAchievementFilesLinux();
|
||||||
|
|
||||||
|
const newAchievementsCount: number[] = [];
|
||||||
|
|
||||||
|
for (const { game, achievementFiles } of gameAchievementFiles) {
|
||||||
|
const result = await this.preProcessGameAchievementFiles(
|
||||||
|
game,
|
||||||
|
achievementFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
newAchievementsCount.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
||||||
(achievements) => achievements
|
(achievements) => achievements
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const totalNewAchievements = newAchievementsCount.reduce(
|
const totalNewAchievements = newAchievementsCount.reduce(
|
||||||
(acc, val) => acc + val,
|
(acc, val) => acc + val,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (totalNewAchievements > 0) {
|
if (totalNewAchievements > 0) {
|
||||||
publishCombinedNewAchievementNotification(
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
totalNewAchievements,
|
levelKeys.userPreferences,
|
||||||
totalNewGamesWithAchievements
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (userPreferences.achievementNotificationsEnabled !== false) {
|
||||||
|
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||||
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
|
"on-combined-achievements-unlocked",
|
||||||
|
totalNewGamesWithAchievements,
|
||||||
|
totalNewAchievements,
|
||||||
|
userPreferences.achievementCustomNotificationPosition ??
|
||||||
|
"top-left"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
publishCombinedNewAchievementNotification(
|
||||||
|
totalNewAchievements,
|
||||||
|
totalNewGamesWithAchievements
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
achievementsLogger.error("Error on preSearchAchievements", err);
|
achievementsLogger.error("Error on preSearchAchievements", err);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const getGameAchievementData = async (
|
|||||||
|
|
||||||
const language = await db
|
const language = await db
|
||||||
.get<string, string>(levelKeys.language, {
|
.get<string, string>(levelKeys.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf8",
|
||||||
})
|
})
|
||||||
.then((language) => language || "en");
|
.then((language) => language || "en");
|
||||||
|
|
||||||
@@ -38,7 +38,9 @@ export const getGameAchievementData = async (
|
|||||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||||
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||||
achievements,
|
achievements,
|
||||||
cacheExpiresTimestamp: Date.now() + 1000 * 60 * 30, // 30 minutes
|
cacheExpiresTimestamp: achievements.length
|
||||||
|
? Date.now() + 1000 * 60 * 30 // 30 minutes
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return achievements;
|
return achievements;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AchievementNotificationInfo,
|
||||||
Game,
|
Game,
|
||||||
GameShop,
|
GameShop,
|
||||||
UnlockedAchievement,
|
UnlockedAchievement,
|
||||||
@@ -12,6 +13,13 @@ import { publishNewAchievementNotification } from "../notifications";
|
|||||||
import { SubscriptionRequiredError } from "@shared";
|
import { SubscriptionRequiredError } from "@shared";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { getGameAchievementData } from "./get-game-achievement-data";
|
||||||
|
|
||||||
|
const isRareAchievement = (points: number) => {
|
||||||
|
const rawPercentage = (50 - Math.sqrt(points)) * 2;
|
||||||
|
|
||||||
|
return rawPercentage < 10;
|
||||||
|
};
|
||||||
|
|
||||||
const saveAchievementsOnLocal = async (
|
const saveAchievementsOnLocal = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -48,12 +56,22 @@ export const mergeAchievements = async (
|
|||||||
achievements: UnlockedAchievement[],
|
achievements: UnlockedAchievement[],
|
||||||
publishNotification: boolean
|
publishNotification: boolean
|
||||||
) => {
|
) => {
|
||||||
const [localGameAchievement, userPreferences] = await Promise.all([
|
let localGameAchievement = await gameAchievementsSublevel.get(
|
||||||
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectId)),
|
levelKeys.game(game.shop, game.objectId)
|
||||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
);
|
||||||
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
}),
|
}
|
||||||
]);
|
);
|
||||||
|
|
||||||
|
if (!localGameAchievement) {
|
||||||
|
await getGameAchievementData(game.objectId, game.shop, true);
|
||||||
|
localGameAchievement = await gameAchievementsSublevel.get(
|
||||||
|
levelKeys.game(game.shop, game.objectId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const achievementsData = localGameAchievement?.achievements ?? [];
|
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||||
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
||||||
@@ -84,9 +102,9 @@ export const mergeAchievements = async (
|
|||||||
if (
|
if (
|
||||||
newAchievements.length &&
|
newAchievements.length &&
|
||||||
publishNotification &&
|
publishNotification &&
|
||||||
userPreferences?.achievementNotificationsEnabled
|
userPreferences.achievementNotificationsEnabled !== false
|
||||||
) {
|
) {
|
||||||
const achievementsInfo = newAchievements
|
const filteredAchievements = newAchievements
|
||||||
.toSorted((a, b) => {
|
.toSorted((a, b) => {
|
||||||
return a.unlockTime - b.unlockTime;
|
return a.unlockTime - b.unlockTime;
|
||||||
})
|
})
|
||||||
@@ -98,21 +116,41 @@ export const mergeAchievements = async (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.filter((achievement) => Boolean(achievement))
|
.filter((achievement) => !!achievement);
|
||||||
.map((achievement) => {
|
|
||||||
|
const achievementsInfo: AchievementNotificationInfo[] =
|
||||||
|
filteredAchievements.map((achievement, index) => {
|
||||||
return {
|
return {
|
||||||
displayName: achievement!.displayName,
|
title: achievement.displayName,
|
||||||
iconUrl: achievement!.icon,
|
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({
|
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||||
achievements: achievementsInfo,
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
unlockedAchievementCount: mergedLocalAchievements.length,
|
"on-achievement-unlocked",
|
||||||
totalAchievementCount: achievementsData.length,
|
userPreferences.achievementCustomNotificationPosition ?? "top-left",
|
||||||
gameTitle: game.title,
|
achievementsInfo
|
||||||
gameIcon: game.iconUrl,
|
);
|
||||||
});
|
} else {
|
||||||
|
publishNewAchievementNotification({
|
||||||
|
achievements: achievementsInfo,
|
||||||
|
unlockedAchievementCount: mergedLocalAchievements.length,
|
||||||
|
totalAchievementCount: achievementsData.length,
|
||||||
|
gameTitle: game.title,
|
||||||
|
gameIcon: game.iconUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.remoteId) {
|
if (game.remoteId) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export class Aria2 {
|
|||||||
|
|
||||||
public static spawn() {
|
public static spawn() {
|
||||||
const binaryPath = app.isPackaged
|
const binaryPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
? path.join(process.resourcesPath, "aria2c")
|
||||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
||||||
|
|
||||||
this.process = cp.spawn(
|
this.process = cp.spawn(
|
||||||
binaryPath,
|
binaryPath,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import os from "node:os";
|
|||||||
import type { GameShop, User } from "@types";
|
import type { GameShop, User } from "@types";
|
||||||
import { backupsPath } from "@main/constants";
|
import { backupsPath } from "@main/constants";
|
||||||
import { HydraApi } from "./hydra-api";
|
import { HydraApi } from "./hydra-api";
|
||||||
import { normalizePath } from "@main/helpers";
|
import { normalizePath, parseRegFile } from "@main/helpers";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -17,6 +17,39 @@ import i18next, { t } from "i18next";
|
|||||||
import { SystemPath } from "./system-path";
|
import { SystemPath } from "./system-path";
|
||||||
|
|
||||||
export class CloudSync {
|
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) {
|
public static getBackupLabel(automatic: boolean) {
|
||||||
const language = i18next.language;
|
const language = i18next.language;
|
||||||
|
|
||||||
@@ -102,9 +135,12 @@ export class CloudSync {
|
|||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
homeDir: normalizePath(SystemPath.getPath("home")),
|
winePrefixPath: game?.winePrefixPath
|
||||||
|
? fs.realpathSync(game.winePrefixPath)
|
||||||
|
: null,
|
||||||
|
homeDir: this.getWindowsLikeUserProfilePath(game?.winePrefixPath ?? null),
|
||||||
downloadOptionTitle,
|
downloadOptionTitle,
|
||||||
platform: os.platform(),
|
platform: process.platform,
|
||||||
label,
|
label,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export * from "./download-manager";
|
export * from "./download-manager";
|
||||||
|
export * from "./real-debrid";
|
||||||
|
export * from "./torbox";
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getUserData } from "./user/get-user-data";
|
|||||||
import { db } from "@main/level";
|
import { db } from "@main/level";
|
||||||
import { levelKeys } from "@main/level/sublevels";
|
import { levelKeys } from "@main/level/sublevels";
|
||||||
import type { Auth, User } from "@types";
|
import type { Auth, User } from "@types";
|
||||||
|
import { WSClient } from "./ws/ws-client";
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
@@ -41,7 +42,7 @@ export class HydraApi {
|
|||||||
subscription: null,
|
subscription: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static isLoggedIn() {
|
public static isLoggedIn() {
|
||||||
return this.userAuth.authToken !== "";
|
return this.userAuth.authToken !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +102,8 @@ export class HydraApi {
|
|||||||
WindowManager.mainWindow.webContents.send("on-signin");
|
WindowManager.mainWindow.webContents.send("on-signin");
|
||||||
await clearGamesRemoteIds();
|
await clearGamesRemoteIds();
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
|
WSClient.close();
|
||||||
|
WSClient.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,7 @@ export * from "./7zip";
|
|||||||
export * from "./game-files-manager";
|
export * from "./game-files-manager";
|
||||||
export * from "./common-redist-manager";
|
export * from "./common-redist-manager";
|
||||||
export * from "./aria2";
|
export * from "./aria2";
|
||||||
|
export * from "./ws";
|
||||||
|
export * from "./system-path";
|
||||||
|
export * from "./library-sync";
|
||||||
|
export * from "./wine";
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import { ShopAssets } from "@types";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { gamesShopAssetsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
type ProfileGame = {
|
||||||
|
id: string;
|
||||||
|
lastTimePlayed: Date | null;
|
||||||
|
playTimeInMilliseconds: number;
|
||||||
|
} & ShopAssets;
|
||||||
|
|
||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
return HydraApi.get("/profile/games")
|
return HydraApi.get<ProfileGame[]>("/profile/games")
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
for (const game of response) {
|
for (const game of response) {
|
||||||
const localGame = await gamesSublevel.get(
|
const localGame = await gamesSublevel.get(
|
||||||
@@ -31,25 +36,32 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
playTimeInMilliseconds: updatedPlayTime,
|
playTimeInMilliseconds: updatedPlayTime,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: steamGame?.name,
|
title: game.title,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
iconUrl,
|
iconUrl: game.iconUrl,
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||||
isDeleted: false,
|
isDeleted: 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,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const uploadGamesBatch = async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const gamesChunks = chunk(games, 50);
|
const gamesChunks = chunk(games, 30);
|
||||||
|
|
||||||
for (const chunk of gamesChunks) {
|
for (const chunk of gamesChunks) {
|
||||||
await HydraApi.post(
|
await HydraApi.post(
|
||||||
@@ -33,7 +33,9 @@ export const uploadGamesBatch = async () => {
|
|||||||
|
|
||||||
await mergeWithRemoteGames();
|
await mergeWithRemoteGames();
|
||||||
|
|
||||||
AchievementWatcherManager.preSearchAchievements();
|
if (HydraApi.isLoggedIn()) {
|
||||||
|
AchievementWatcherManager.preSearchAchievements();
|
||||||
|
}
|
||||||
|
|
||||||
if (WindowManager.mainWindow)
|
if (WindowManager.mainWindow)
|
||||||
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
||||||
|
|||||||
@@ -1,70 +1,89 @@
|
|||||||
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
|
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
|
||||||
import Piscina from "piscina";
|
|
||||||
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
|
import cp from "node:child_process";
|
||||||
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
|
||||||
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
|
|
||||||
import { SystemPath } from "./system-path";
|
import { SystemPath } from "./system-path";
|
||||||
|
|
||||||
export class Ludusavi {
|
export class Ludusavi {
|
||||||
private static ludusaviPath = path.join(
|
private static ludusaviPath = app.isPackaged
|
||||||
SystemPath.getPath("appData"),
|
? path.join(process.resourcesPath, "ludusavi")
|
||||||
|
: path.join(__dirname, "..", "..", "ludusavi");
|
||||||
|
|
||||||
|
private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
|
||||||
|
private static configPath = path.join(
|
||||||
|
SystemPath.getPath("userData"),
|
||||||
"ludusavi"
|
"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 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" });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public static async getConfig() {
|
||||||
const config = YAML.parse(
|
const config = YAML.parse(
|
||||||
fs.readFileSync(this.ludusaviConfigPath, "utf-8")
|
fs.readFileSync(path.join(this.ludusaviPath, "config.yaml"), "utf-8")
|
||||||
) as LudusaviConfig;
|
) as LudusaviConfig;
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async backupGame(
|
public static async copyConfigFileToUserData() {
|
||||||
_shop: GameShop,
|
if (!fs.existsSync(this.configPath)) {
|
||||||
objectId: string,
|
fs.mkdirSync(this.configPath, { recursive: true });
|
||||||
backupPath: string,
|
fs.cpSync(
|
||||||
winePrefix?: string | null
|
path.join(this.ludusaviPath, "config.yaml"),
|
||||||
): Promise<LudusaviBackup> {
|
path.join(this.configPath, "config.yaml")
|
||||||
return this.worker.run(
|
);
|
||||||
{ title: objectId, backupPath, winePrefix },
|
}
|
||||||
{ name: "backupGame" }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getBackupPreview(
|
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,
|
_shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
winePrefix?: string | null
|
winePrefix?: string | null
|
||||||
): Promise<LudusaviBackup | null> {
|
): Promise<LudusaviBackup | null> {
|
||||||
const config = await this.getConfig();
|
const config = await this.getConfig();
|
||||||
|
|
||||||
const backupData = await this.worker.run(
|
const backupData = await this.backupGame(
|
||||||
{ title: objectId, winePrefix, preview: true },
|
_shop,
|
||||||
{ name: "backupGame" }
|
objectId,
|
||||||
|
null,
|
||||||
|
winePrefix,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const customGame = config.customGames.find(
|
const customGame = config.customGames.find(
|
||||||
@@ -77,19 +96,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) {
|
static async addCustomGame(title: string, savePath: string | null) {
|
||||||
const config = await this.getConfig();
|
const config = await this.getConfig();
|
||||||
const filteredGames = config.customGames.filter(
|
const filteredGames = config.customGames.filter(
|
||||||
@@ -105,6 +111,10 @@ export class Ludusavi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.customGames = filteredGames;
|
config.customGames = filteredGames;
|
||||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(this.configPath, "config.yaml"),
|
||||||
|
YAML.stringify(config)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { DownloadManager } from "./download";
|
|||||||
import { watchProcesses } from "./process-watcher";
|
import { watchProcesses } from "./process-watcher";
|
||||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||||
import { UpdateManager } from "./update-manager";
|
import { UpdateManager } from "./update-manager";
|
||||||
import { watchFriendRequests } from "@main/events/profile/sync-friend-requests";
|
|
||||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||||
|
|
||||||
export const startMainLoop = async () => {
|
export const startMainLoop = async () => {
|
||||||
@@ -11,7 +10,6 @@ export const startMainLoop = async () => {
|
|||||||
while (true) {
|
while (true) {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
watchProcesses(),
|
watchProcesses(),
|
||||||
watchFriendRequests(),
|
|
||||||
DownloadManager.watchDownloads(),
|
DownloadManager.watchDownloads(),
|
||||||
AchievementWatcherManager.watchAchievements(),
|
AchievementWatcherManager.watchAchievements(),
|
||||||
DownloadManager.getSeedStatus(),
|
DownloadManager.getSeedStatus(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import icon from "@resources/icon.png?asset";
|
|||||||
import { NotificationOptions, toXmlString } from "./xml";
|
import { NotificationOptions, toXmlString } from "./xml";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import type { Game, UserPreferences } from "@types";
|
import type { Game, GameStats, UserPreferences, UserProfile } from "@types";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
|
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
|
||||||
import { SystemPath } from "../system-path";
|
import { SystemPath } from "../system-path";
|
||||||
@@ -81,7 +81,9 @@ export const publishNotificationUpdateReadyToInstall = async (
|
|||||||
.show();
|
.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const publishNewFriendRequestNotification = async () => {
|
export const publishNewFriendRequestNotification = async (
|
||||||
|
user: UserProfile
|
||||||
|
) => {
|
||||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
@@ -97,8 +99,27 @@ export const publishNewFriendRequestNotification = async () => {
|
|||||||
}),
|
}),
|
||||||
body: t("new_friend_request_description", {
|
body: t("new_friend_request_description", {
|
||||||
ns: "notifications",
|
ns: "notifications",
|
||||||
|
displayName: user.displayName,
|
||||||
}),
|
}),
|
||||||
icon: trayIcon,
|
icon: user?.profileImageUrl
|
||||||
|
? await downloadImage(user.profileImageUrl)
|
||||||
|
: trayIcon,
|
||||||
|
}).show();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishFriendStartedPlayingGameNotification = async (
|
||||||
|
friend: UserProfile,
|
||||||
|
game: GameStats
|
||||||
|
) => {
|
||||||
|
new Notification({
|
||||||
|
title: t("friend_started_playing_game", {
|
||||||
|
ns: "notifications",
|
||||||
|
displayName: friend.displayName,
|
||||||
|
}),
|
||||||
|
body: game.assets?.title,
|
||||||
|
icon: friend?.profileImageUrl
|
||||||
|
? await downloadImage(friend.profileImageUrl)
|
||||||
|
: trayIcon,
|
||||||
}).show();
|
}).show();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,7 +162,7 @@ export const publishExtractionCompleteNotification = async (game: Game) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const publishNewAchievementNotification = async (info: {
|
export const publishNewAchievementNotification = async (info: {
|
||||||
achievements: { displayName: string; iconUrl: string }[];
|
achievements: { title: string; iconUrl: string }[];
|
||||||
unlockedAchievementCount: number;
|
unlockedAchievementCount: number;
|
||||||
totalAchievementCount: number;
|
totalAchievementCount: number;
|
||||||
gameTitle: string;
|
gameTitle: string;
|
||||||
@@ -155,12 +176,12 @@ export const publishNewAchievementNotification = async (info: {
|
|||||||
gameTitle: info.gameTitle,
|
gameTitle: info.gameTitle,
|
||||||
achievementCount: info.achievements.length,
|
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,
|
icon: (await downloadImage(info.gameIcon)) ?? icon,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: t("achievement_unlocked", { ns: "achievement" }),
|
title: t("achievement_unlocked", { ns: "achievement" }),
|
||||||
body: info.achievements[0].displayName,
|
body: info.achievements[0].title,
|
||||||
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
|
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import crypto from "node:crypto";
|
|||||||
import { pythonRpcLogger } from "./logger";
|
import { pythonRpcLogger } from "./logger";
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import { app, dialog } from "electron";
|
import { app, dialog } from "electron";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
interface GamePayload {
|
interface GamePayload {
|
||||||
game_id: string;
|
game_id: string;
|
||||||
@@ -21,26 +22,15 @@ const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
|||||||
win32: "hydra-python-rpc.exe",
|
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 {
|
export class PythonRPC {
|
||||||
public static readonly BITTORRENT_PORT = "5881";
|
public static readonly BITTORRENT_PORT = "5881";
|
||||||
public static readonly RPC_PORT = "8084";
|
public static readonly RPC_PORT = "8084";
|
||||||
private static readonly RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
|
||||||
|
|
||||||
private static pythonProcess: cp.ChildProcess | null = null;
|
|
||||||
|
|
||||||
public static readonly rpc = axios.create({
|
public static readonly rpc = axios.create({
|
||||||
baseURL: `http://localhost:${this.RPC_PORT}`,
|
baseURL: `http://localhost:${this.RPC_PORT}`,
|
||||||
headers: {
|
|
||||||
"x-hydra-rpc-password": this.RPC_PASSWORD,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private static pythonProcess: cp.ChildProcess | null = null;
|
||||||
|
|
||||||
private static logStderr(readable: Readable | null) {
|
private static logStderr(readable: Readable | null) {
|
||||||
if (!readable) return;
|
if (!readable) return;
|
||||||
|
|
||||||
@@ -48,30 +38,34 @@ export class PythonRPC {
|
|||||||
readable.on("data", pythonRpcLogger.log);
|
readable.on("data", pythonRpcLogger.log);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static spawn(
|
private static async getRPCPassword() {
|
||||||
|
const existingPassword = await db.get(levelKeys.rpcPassword, {
|
||||||
|
valueEncoding: "utf8",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingPassword) return existingPassword;
|
||||||
|
|
||||||
|
const newPassword = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
|
await db.put(levelKeys.rpcPassword, newPassword, {
|
||||||
|
valueEncoding: "utf8",
|
||||||
|
});
|
||||||
|
|
||||||
|
return newPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async spawn(
|
||||||
initialDownload?: GamePayload,
|
initialDownload?: GamePayload,
|
||||||
initialSeeding?: GamePayload[]
|
initialSeeding?: GamePayload[]
|
||||||
) {
|
) {
|
||||||
|
const rpcPassword = await this.getRPCPassword();
|
||||||
|
|
||||||
const commonArgs = [
|
const commonArgs = [
|
||||||
this.BITTORRENT_PORT,
|
this.BITTORRENT_PORT,
|
||||||
this.RPC_PORT,
|
this.RPC_PORT,
|
||||||
this.RPC_PASSWORD,
|
rpcPassword,
|
||||||
initialDownload ? JSON.stringify(initialDownload) : "",
|
initialDownload ? JSON.stringify(initialDownload) : "",
|
||||||
initialSeeding ? JSON.stringify(initialSeeding) : "",
|
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) {
|
if (app.isPackaged) {
|
||||||
@@ -116,6 +110,8 @@ export class PythonRPC {
|
|||||||
|
|
||||||
this.pythonProcess = childProcess;
|
this.pythonProcess = childProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.rpc.defaults.headers.common["x-hydra-rpc-password"] = rpcPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static kill() {
|
public static kill() {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import axios from "axios";
|
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 { logger } from "./logger";
|
||||||
|
import { SystemPath } from "./system-path";
|
||||||
|
|
||||||
export interface SteamAppDetailsResponse {
|
export interface SteamAppDetailsResponse {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@@ -11,6 +17,36 @@ export interface SteamAppDetailsResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSteamLocation = async () => {
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
return path.join(SystemPath.getPath("home"), ".local", "share", "Steam");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
return path.join(
|
||||||
|
SystemPath.getPath("home"),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Steam"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const regKey = new WinReg({
|
||||||
|
hive: WinReg.HKCU,
|
||||||
|
key: "\\Software\\Valve\\Steam",
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
regKey.get("SteamPath", (err, value) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(value.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const getSteamAppDetails = async (
|
export const getSteamAppDetails = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
language: string
|
language: string
|
||||||
@@ -40,3 +76,86 @@ export const getSteamAppDetails = async (
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSteamUsersIds = async () => {
|
||||||
|
const userDataPath = await getSteamLocation();
|
||||||
|
|
||||||
|
const userIds = fs.readdirSync(path.join(userDataPath, "userdata"), {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return userIds
|
||||||
|
.filter((dir) => dir.isDirectory())
|
||||||
|
.map((dir) => Number(dir.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSteamShortcuts = async (steamUserId: number) => {
|
||||||
|
const shortcutsPath = path.join(
|
||||||
|
await getSteamLocation(),
|
||||||
|
"userdata",
|
||||||
|
steamUserId.toString(),
|
||||||
|
"config",
|
||||||
|
"shortcuts.vdf"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(shortcutsPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcuts = parseBuffer(fs.readFileSync(shortcutsPath));
|
||||||
|
|
||||||
|
return shortcuts.shortcuts as SteamShortcut[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateSteamShortcutAppId = (
|
||||||
|
exePath: string,
|
||||||
|
gameName: string
|
||||||
|
) => {
|
||||||
|
const input = exePath + gameName;
|
||||||
|
const crcValue = crc32(input) >>> 0;
|
||||||
|
const steamAppId = (crcValue | 0x80000000) >>> 0;
|
||||||
|
return steamAppId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const composeSteamShortcut = (
|
||||||
|
title: string,
|
||||||
|
executablePath: string,
|
||||||
|
iconPath: string | null
|
||||||
|
): SteamShortcut => {
|
||||||
|
return {
|
||||||
|
appid: generateSteamShortcutAppId(executablePath, title),
|
||||||
|
appname: title,
|
||||||
|
Exe: `"${executablePath}"`,
|
||||||
|
StartDir: `"${path.dirname(executablePath)}"`,
|
||||||
|
icon: iconPath ?? "",
|
||||||
|
ShortcutPath: "",
|
||||||
|
LaunchOptions: "",
|
||||||
|
IsHidden: false,
|
||||||
|
AllowDesktopConfig: true,
|
||||||
|
AllowOverlay: true,
|
||||||
|
OpenVR: false,
|
||||||
|
Devkit: false,
|
||||||
|
DevkitGameID: "",
|
||||||
|
DevkitOverrideAppID: false,
|
||||||
|
LastPlayTime: 0,
|
||||||
|
FlatpakAppID: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeSteamShortcuts = async (
|
||||||
|
steamUserId: number,
|
||||||
|
shortcuts: SteamShortcut[]
|
||||||
|
) => {
|
||||||
|
const buffer = writeBuffer({ shortcuts });
|
||||||
|
|
||||||
|
return fs.promises.writeFile(
|
||||||
|
path.join(
|
||||||
|
await getSteamLocation(),
|
||||||
|
"userdata",
|
||||||
|
steamUserId.toString(),
|
||||||
|
"config",
|
||||||
|
"shortcuts.vdf"
|
||||||
|
),
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class SystemPath {
|
|||||||
try {
|
try {
|
||||||
return app.getPath(pathName);
|
return app.getPath(pathName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error getting path: ${error}`);
|
console.error(`Error getting path: ${error}`);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Tray,
|
Tray,
|
||||||
app,
|
app,
|
||||||
nativeImage,
|
nativeImage,
|
||||||
|
screen,
|
||||||
shell,
|
shell,
|
||||||
} from "electron";
|
} from "electron";
|
||||||
import { is } from "@electron-toolkit/utils";
|
import { is } from "@electron-toolkit/utils";
|
||||||
@@ -17,12 +18,17 @@ import { HydraApi } from "./hydra-api";
|
|||||||
import UserAgent from "user-agents";
|
import UserAgent from "user-agents";
|
||||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { orderBy, slice } from "lodash-es";
|
import { orderBy, slice } from "lodash-es";
|
||||||
import type { ScreenState, UserPreferences } from "@types";
|
import type {
|
||||||
import { AuthPage } from "@shared";
|
AchievementCustomNotificationPosition,
|
||||||
|
ScreenState,
|
||||||
|
UserPreferences,
|
||||||
|
} from "@types";
|
||||||
|
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
|
||||||
import { isStaging } from "@main/constants";
|
import { isStaging } from "@main/constants";
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||||
|
public static notificationWindow: Electron.BrowserWindow | null = null;
|
||||||
|
|
||||||
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||||
|
|
||||||
@@ -259,6 +265,156 @@ 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.setVisibleOnAllWorkspaces(true, {
|
||||||
|
// visibleOnFullScreen: true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||||
|
this.loadNotificationWindowURL();
|
||||||
|
|
||||||
|
if (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) {
|
public static openEditorWindow(themeId: string) {
|
||||||
if (this.mainWindow) {
|
if (this.mainWindow) {
|
||||||
const existingWindow = this.editorWindows.get(themeId);
|
const existingWindow = this.editorWindows.get(themeId);
|
||||||
@@ -271,13 +427,13 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editorWindow = new BrowserWindow({
|
const editorWindow = new BrowserWindow({
|
||||||
width: 600,
|
width: 720,
|
||||||
height: 720,
|
height: 720,
|
||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
minHeight: 540,
|
minHeight: 540,
|
||||||
backgroundColor: "#1c1c1c",
|
backgroundColor: "#1c1c1c",
|
||||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
...(process.platform === "linux" ? { icon } : {}),
|
icon,
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
titleBarOverlay: {
|
titleBarOverlay: {
|
||||||
symbolColor: "#DADBE1",
|
symbolColor: "#DADBE1",
|
||||||
@@ -313,9 +469,8 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
editorWindow.webContents.on("before-input-event", (event, input) => {
|
editorWindow.webContents.on("before-input-event", (_event, input) => {
|
||||||
if (input.key === "F12") {
|
if (input.key === "F12") {
|
||||||
event.preventDefault();
|
|
||||||
this.mainWindow?.webContents.toggleDevTools();
|
this.mainWindow?.webContents.toggleDevTools();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/services/ws/events/friend-game-session.ts
Normal file
27
src/main/services/ws/events/friend-game-session.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { FriendGameSession } from "@main/generated/envelope";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import { HydraApi } from "@main/services/hydra-api";
|
||||||
|
import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications";
|
||||||
|
import type { GameStats, UserPreferences, UserProfile } from "@types";
|
||||||
|
|
||||||
|
export const friendGameSessionEvent = async (payload: FriendGameSession) => {
|
||||||
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userPreferences?.friendStartGameNotificationsEnabled === false) return;
|
||||||
|
|
||||||
|
const [friend, gameStats] = await Promise.all([
|
||||||
|
HydraApi.get<UserProfile>(`/users/${payload.friendId}`),
|
||||||
|
HydraApi.get<GameStats>(
|
||||||
|
`/games/stats?objectId=${payload.objectId}&shop=steam`
|
||||||
|
),
|
||||||
|
]).catch(() => [null, null]);
|
||||||
|
|
||||||
|
if (friend && gameStats) {
|
||||||
|
publishFriendStartedPlayingGameNotification(friend, gameStats);
|
||||||
|
}
|
||||||
|
};
|
||||||
16
src/main/services/ws/events/friend-request.ts
Normal file
16
src/main/services/ws/events/friend-request.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { FriendRequest } from "@main/generated/envelope";
|
||||||
|
import { HydraApi } from "@main/services/hydra-api";
|
||||||
|
import { publishNewFriendRequestNotification } from "@main/services/notifications";
|
||||||
|
import { WindowManager } from "@main/services/window-manager";
|
||||||
|
|
||||||
|
export const friendRequestEvent = async (payload: FriendRequest) => {
|
||||||
|
WindowManager.mainWindow?.webContents.send("on-sync-friend-requests", {
|
||||||
|
friendRequestCount: payload.friendRequestCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await HydraApi.get(`/users/${payload.senderId}`);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
publishNewFriendRequestNotification(user);
|
||||||
|
}
|
||||||
|
};
|
||||||
1
src/main/services/ws/index.ts
Normal file
1
src/main/services/ws/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ws-client";
|
||||||
119
src/main/services/ws/ws-client.ts
Normal file
119
src/main/services/ws/ws-client.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { WebSocket } from "ws";
|
||||||
|
import { HydraApi } from "../hydra-api";
|
||||||
|
import { Envelope } from "@main/generated/envelope";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { friendRequestEvent } from "./events/friend-request";
|
||||||
|
import { friendGameSessionEvent } from "./events/friend-game-session";
|
||||||
|
|
||||||
|
export class WSClient {
|
||||||
|
private static ws: WebSocket | null = null;
|
||||||
|
private static reconnectInterval = 1_000;
|
||||||
|
private static readonly maxReconnectInterval = 30_000;
|
||||||
|
private static shouldReconnect = true;
|
||||||
|
private static reconnecting = false;
|
||||||
|
private static heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
static async connect() {
|
||||||
|
this.shouldReconnect = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { token } = await HydraApi.post<{ token: string }>("/auth/ws");
|
||||||
|
|
||||||
|
this.ws = new WebSocket(import.meta.env.MAIN_VITE_WS_URL, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on("open", () => {
|
||||||
|
logger.info("WS connected");
|
||||||
|
this.reconnectInterval = 1000;
|
||||||
|
this.reconnecting = false;
|
||||||
|
|
||||||
|
this.startHeartbeat();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on("message", (message) => {
|
||||||
|
const envelope = Envelope.fromBinary(
|
||||||
|
new Uint8Array(Buffer.from(message.toString()))
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("Received WS envelope:", envelope);
|
||||||
|
|
||||||
|
if (envelope.payload.oneofKind === "friendRequest") {
|
||||||
|
friendRequestEvent(envelope.payload.friendRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envelope.payload.oneofKind === "friendGameSession") {
|
||||||
|
friendGameSessionEvent(envelope.payload.friendGameSession);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on("close", () => this.handleDisconnect("close"));
|
||||||
|
this.ws.on("error", (err) => {
|
||||||
|
logger.error("WS error:", err);
|
||||||
|
this.handleDisconnect("error");
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to connect WS:", err);
|
||||||
|
this.handleDisconnect("auth-failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static handleDisconnect(reason: string) {
|
||||||
|
logger.warn(`WS disconnected due to ${reason}`);
|
||||||
|
|
||||||
|
if (this.shouldReconnect) {
|
||||||
|
this.cleanupSocket();
|
||||||
|
this.tryReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async tryReconnect() {
|
||||||
|
if (this.reconnecting) return;
|
||||||
|
this.reconnecting = true;
|
||||||
|
|
||||||
|
logger.info(`Reconnecting in ${this.reconnectInterval / 1000}s...`);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await this.connect();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Reconnect failed:", err);
|
||||||
|
this.reconnectInterval = Math.min(
|
||||||
|
this.reconnectInterval * 2,
|
||||||
|
this.maxReconnectInterval
|
||||||
|
);
|
||||||
|
this.reconnecting = false;
|
||||||
|
this.tryReconnect();
|
||||||
|
}
|
||||||
|
}, this.reconnectInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static cleanupSocket() {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.removeAllListeners();
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static close() {
|
||||||
|
this.shouldReconnect = false;
|
||||||
|
this.reconnecting = false;
|
||||||
|
this.cleanupSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static startHeartbeat() {
|
||||||
|
this.heartbeatInterval = setInterval(() => {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.ping();
|
||||||
|
}
|
||||||
|
}, 15_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/main/vite-env.d.ts
vendored
1
src/main/vite-env.d.ts
vendored
@@ -6,6 +6,7 @@ interface ImportMetaEnv {
|
|||||||
readonly MAIN_VITE_AUTH_URL: string;
|
readonly MAIN_VITE_AUTH_URL: string;
|
||||||
readonly MAIN_VITE_CHECKOUT_URL: string;
|
readonly MAIN_VITE_CHECKOUT_URL: string;
|
||||||
readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string;
|
readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string;
|
||||||
|
readonly MAIN_VITE_WS_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
|
||||||
|
|
||||||
import Piscina from "piscina";
|
|
||||||
|
|
||||||
import { seedsPath } from "@main/constants";
|
|
||||||
|
|
||||||
export const steamGamesWorker = new Piscina({
|
|
||||||
filename: steamGamesWorkerPath,
|
|
||||||
workerData: {
|
|
||||||
steamGamesPath: path.join(seedsPath, "steam-games.json"),
|
|
||||||
},
|
|
||||||
maxThreads: 1,
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { SteamGame } from "@types";
|
|
||||||
import { slice } from "lodash-es";
|
|
||||||
import fs from "node:fs";
|
|
||||||
|
|
||||||
import { workerData } from "node:worker_threads";
|
|
||||||
|
|
||||||
const { steamGamesPath } = workerData;
|
|
||||||
|
|
||||||
const data = fs.readFileSync(steamGamesPath, "utf-8");
|
|
||||||
|
|
||||||
const steamGames = JSON.parse(data) as SteamGame[];
|
|
||||||
|
|
||||||
export const getById = (id: number) =>
|
|
||||||
steamGames.find((game) => game.id === id);
|
|
||||||
|
|
||||||
export const list = ({ limit, offset }: { limit: number; offset: number }) =>
|
|
||||||
slice(steamGames, offset, offset + limit);
|
|
||||||
@@ -17,6 +17,9 @@ import type {
|
|||||||
Theme,
|
Theme,
|
||||||
FriendRequestSync,
|
FriendRequestSync,
|
||||||
ShortcutLocation,
|
ShortcutLocation,
|
||||||
|
ShopAssets,
|
||||||
|
AchievementCustomNotificationPosition,
|
||||||
|
AchievementNotificationInfo,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
@@ -64,6 +67,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("searchGames", payload, take, skip),
|
ipcRenderer.invoke("searchGames", payload, take, skip),
|
||||||
getCatalogue: (category: CatalogueCategory) =>
|
getCatalogue: (category: CatalogueCategory) =>
|
||||||
ipcRenderer.invoke("getCatalogue", category),
|
ipcRenderer.invoke("getCatalogue", category),
|
||||||
|
saveGameShopAssets: (objectId: string, shop: GameShop, assets: ShopAssets) =>
|
||||||
|
ipcRenderer.invoke("saveGameShopAssets", objectId, shop, assets),
|
||||||
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
|
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
|
||||||
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
|
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
|
||||||
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
||||||
@@ -186,6 +191,10 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
||||||
|
getDefaultWinePrefixSelectionPath: () =>
|
||||||
|
ipcRenderer.invoke("getDefaultWinePrefixSelectionPath"),
|
||||||
|
createSteamShortcut: (shop: GameShop, objectId: string) =>
|
||||||
|
ipcRenderer.invoke("createSteamShortcut", shop, objectId),
|
||||||
onGamesRunning: (
|
onGamesRunning: (
|
||||||
cb: (
|
cb: (
|
||||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||||
@@ -202,12 +211,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
return () =>
|
return () =>
|
||||||
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
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) => {
|
onExtractionComplete: (cb: (shop: GameShop, objectId: string) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
@@ -405,6 +408,42 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
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 */
|
/* Themes */
|
||||||
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),
|
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),
|
||||||
@@ -423,11 +462,11 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
/* Editor */
|
/* Editor */
|
||||||
openEditorWindow: (themeId: string) =>
|
openEditorWindow: (themeId: string) =>
|
||||||
ipcRenderer.invoke("openEditorWindow", themeId),
|
ipcRenderer.invoke("openEditorWindow", themeId),
|
||||||
onCssInjected: (cb: (cssString: string) => void) => {
|
onCustomThemeUpdated: (cb: () => void) => {
|
||||||
const listener = (_event: Electron.IpcRendererEvent, cssString: string) =>
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
cb(cssString);
|
ipcRenderer.on("on-custom-theme-updated", listener);
|
||||||
ipcRenderer.on("css-injected", listener);
|
return () =>
|
||||||
return () => ipcRenderer.removeListener("css-injected", listener);
|
ipcRenderer.removeListener("on-custom-theme-updated", listener);
|
||||||
},
|
},
|
||||||
closeEditorWindow: (themeId?: string) =>
|
closeEditorWindow: (themeId?: string) =>
|
||||||
ipcRenderer.invoke("closeEditorWindow", themeId),
|
ipcRenderer.invoke("closeEditorWindow", themeId),
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
setUserDetails,
|
setUserDetails,
|
||||||
setProfileBackground,
|
setProfileBackground,
|
||||||
setGameRunning,
|
setGameRunning,
|
||||||
setFriendRequestCount,
|
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||||
@@ -29,7 +28,7 @@ import { downloadSourcesTable } from "./dexie";
|
|||||||
import { useSubscription } from "./hooks/use-subscription";
|
import { useSubscription } from "./hooks/use-subscription";
|
||||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||||
|
|
||||||
import { injectCustomCss } from "./helpers";
|
import { injectCustomCss, removeCustomCss } from "./helpers";
|
||||||
import "./app.scss";
|
import "./app.scss";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
@@ -155,16 +154,6 @@ export function App() {
|
|||||||
});
|
});
|
||||||
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
|
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
|
|
||||||
dispatch(setFriendRequestCount(result.friendRequestCount));
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
||||||
if (gamesRunning.length) {
|
if (gamesRunning.length) {
|
||||||
@@ -257,17 +246,27 @@ export function App() {
|
|||||||
};
|
};
|
||||||
}, [updateRepacks]);
|
}, [updateRepacks]);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadAndApplyTheme = useCallback(async () => {
|
||||||
const loadAndApplyTheme = async () => {
|
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||||
const activeTheme = await window.electron.getActiveCustomTheme();
|
if (activeTheme?.code) {
|
||||||
|
injectCustomCss(activeTheme.code);
|
||||||
if (activeTheme?.code) {
|
} else {
|
||||||
injectCustomCss(activeTheme.code);
|
removeCustomCss();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
loadAndApplyTheme();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAndApplyTheme();
|
||||||
|
}, [loadAndApplyTheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onCustomThemeUpdated(() => {
|
||||||
|
loadAndApplyTheme();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [loadAndApplyTheme]);
|
||||||
|
|
||||||
const playAudio = useCallback(() => {
|
const playAudio = useCallback(() => {
|
||||||
const audio = new Audio(achievementSound);
|
const audio = new Audio(achievementSound);
|
||||||
audio.volume = 0.2;
|
audio.volume = 0.2;
|
||||||
@@ -284,14 +283,6 @@ export function App() {
|
|||||||
};
|
};
|
||||||
}, [playAudio]);
|
}, [playAudio]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
|
||||||
injectCustomCss(cssString);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => unsubscribe();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
dispatch(closeToast());
|
dispatch(closeToast());
|
||||||
}, [dispatch]);
|
}, [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 cn from "classnames";
|
||||||
|
import { PlacesType, Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
import "./button.scss";
|
import "./button.scss";
|
||||||
|
import { useId } from "react";
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
HTMLButtonElement
|
HTMLButtonElement
|
||||||
> {
|
> {
|
||||||
|
tooltip?: string;
|
||||||
|
tooltipPlace?: PlacesType;
|
||||||
theme?: "primary" | "outline" | "dark" | "danger";
|
theme?: "primary" | "outline" | "dark" | "danger";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,15 +18,32 @@ export function Button({
|
|||||||
children,
|
children,
|
||||||
theme = "primary",
|
theme = "primary",
|
||||||
className,
|
className,
|
||||||
|
tooltip,
|
||||||
|
tooltipPlace = "top",
|
||||||
...props
|
...props
|
||||||
}: Readonly<ButtonProps>) {
|
}: Readonly<ButtonProps>) {
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
const tooltipProps = tooltip
|
||||||
|
? {
|
||||||
|
"data-tooltip-id": id,
|
||||||
|
"data-tooltip-place": tooltipPlace,
|
||||||
|
"data-tooltip-content": tooltip,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<button
|
||||||
className={cn("button", `button--${theme}`, className)}
|
type="button"
|
||||||
{...props}
|
className={cn("button", `button--${theme}`, className)}
|
||||||
>
|
{...props}
|
||||||
{children}
|
{...tooltipProps}
|
||||||
</button>
|
>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
import { useCallback, useState, useMemo } from "react";
|
import { useCallback, useState, useMemo } from "react";
|
||||||
import { useFormat, useRepacks } from "@renderer/hooks";
|
import { useFormat, useRepacks } from "@renderer/hooks";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
|
|
||||||
export interface GameCardProps
|
export interface GameCardProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
@@ -63,7 +62,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
|||||||
>
|
>
|
||||||
<div className="game-card__backdrop">
|
<div className="game-card__backdrop">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
src={game.libraryImageUrl}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
className="game-card__cover"
|
className="game-card__cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -33,29 +33,27 @@ export function Hero() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (featuredGameDetails?.length) {
|
if (featuredGameDetails?.length) {
|
||||||
return featuredGameDetails.map((game, index) => (
|
return featuredGameDetails.map((game) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(game.uri)}
|
onClick={() => navigate(game.uri)}
|
||||||
className="hero"
|
className="hero"
|
||||||
key={index}
|
key={game.uri}
|
||||||
>
|
>
|
||||||
<div className="hero__backdrop">
|
<div className="hero__backdrop">
|
||||||
<img
|
<img
|
||||||
src={game.background}
|
src={game.libraryHeroImageUrl}
|
||||||
alt={game.description}
|
alt={game.description ?? ""}
|
||||||
className="hero__media"
|
className="hero__media"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="hero__content">
|
<div className="hero__content">
|
||||||
{game.logo && (
|
<img
|
||||||
<img
|
src={game.logoImageUrl}
|
||||||
src={game.logo}
|
width="250px"
|
||||||
width="250px"
|
alt={game.description ?? ""}
|
||||||
alt={game.description}
|
loading="eager"
|
||||||
loading="eager"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p className="hero__description">{game.description}</p>
|
<p className="hero__description">{game.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ export function SelectField({
|
|||||||
options = [{ key: "-", value: value?.toString() || "-", label: "-" }],
|
options = [{ key: "-", value: value?.toString() || "-", label: "-" }],
|
||||||
theme = "primary",
|
theme = "primary",
|
||||||
onChange,
|
onChange,
|
||||||
}: SelectProps) {
|
className,
|
||||||
|
}: Readonly<SelectProps>) {
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="select-field__container">
|
<div className={cn("select-field__container", className)}>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={id} className="select-field__label">
|
<label htmlFor={id} className="select-field__label">
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { sortBy } from "lodash-es";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||||
import { SidebarGameItem } from "./sidebar-game-item";
|
import { SidebarGameItem } from "./sidebar-game-item";
|
||||||
|
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@@ -33,6 +35,8 @@ const initialSidebarWidth = window.localStorage.getItem("sidebarWidth");
|
|||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const filterRef = useRef<HTMLInputElement>(null);
|
const filterRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
const { library, updateLibrary } = useLibrary();
|
const { library, updateLibrary } = useLibrary();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -60,6 +64,16 @@ export function Sidebar() {
|
|||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [lastPacket?.gameId, updateLibrary]);
|
}, [lastPacket?.gameId, updateLibrary]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
|
||||||
|
dispatch(setFriendRequestCount(result.friendRequestCount));
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const sidebarRef = useRef<HTMLElement>(null);
|
const sidebarRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
const cursorPos = useRef({ x: 0 });
|
const cursorPos = useRef({ x: 0 });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
|
|
||||||
export const VERSION_CODENAME = "Polychrome";
|
export const VERSION_CODENAME = "Lumen";
|
||||||
|
|
||||||
export const DOWNLOADER_NAME = {
|
export const DOWNLOADER_NAME = {
|
||||||
[Downloader.RealDebrid]: "Real-Debrid",
|
[Downloader.RealDebrid]: "Real-Debrid",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type {
|
|||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
ShopDetails,
|
ShopDetailsWithAssets,
|
||||||
UserAchievement,
|
UserAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
@@ -69,7 +69,9 @@ export function GameDetailsContextProvider({
|
|||||||
gameTitle,
|
gameTitle,
|
||||||
shop,
|
shop,
|
||||||
}: Readonly<GameDetailsContextProps>) {
|
}: Readonly<GameDetailsContextProps>) {
|
||||||
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
const [shopDetails, setShopDetails] = useState<ShopDetailsWithAssets | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [achievements, setAchievements] = useState<UserAchievement[] | null>(
|
const [achievements, setAchievements] = useState<UserAchievement[] | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -79,7 +81,7 @@ export function GameDetailsContextProvider({
|
|||||||
|
|
||||||
const [stats, setStats] = useState<GameStats | null>(null);
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [gameColor, setGameColor] = useState("");
|
const [gameColor, setGameColor] = useState("");
|
||||||
const [isGameRunning, setIsGameRunning] = useState(false);
|
const [isGameRunning, setIsGameRunning] = useState(false);
|
||||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||||
@@ -120,7 +122,7 @@ export function GameDetailsContextProvider({
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
window.electron
|
const shopDetailsPromise = window.electron
|
||||||
.getGameShopDetails(objectId, shop, getSteamLanguage(i18n.language))
|
.getGameShopDetails(objectId, shop, getSteamLanguage(i18n.language))
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (abortController.signal.aborted) return;
|
if (abortController.signal.aborted) return;
|
||||||
@@ -135,15 +137,41 @@ export function GameDetailsContextProvider({
|
|||||||
) {
|
) {
|
||||||
setHasNSFWContentBlocked(true);
|
setHasNSFWContentBlocked(true);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.finally(() => {
|
if (result?.assets) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electron.getGameStats(objectId, shop).then((result) => {
|
const statsPromise = window.electron
|
||||||
if (abortController.signal.aborted) return;
|
.getGameStats(objectId, shop)
|
||||||
setStats(result);
|
.then((result) => {
|
||||||
});
|
if (abortController.signal.aborted) return null;
|
||||||
|
setStats(result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([shopDetailsPromise, statsPromise])
|
||||||
|
.then(([_, stats]) => {
|
||||||
|
if (stats) {
|
||||||
|
const assets = stats.assets;
|
||||||
|
if (assets) {
|
||||||
|
window.electron.saveGameShopAssets(objectId, shop, assets);
|
||||||
|
|
||||||
|
setShopDetails((prev) => {
|
||||||
|
if (!prev) return null;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
assets,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
if (userDetails) {
|
if (userDetails) {
|
||||||
window.electron
|
window.electron
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import type {
|
|||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
ShopDetails,
|
ShopDetailsWithAssets,
|
||||||
UserAchievement,
|
UserAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
export interface GameDetailsContext {
|
export interface GameDetailsContext {
|
||||||
game: LibraryGame | null;
|
game: LibraryGame | null;
|
||||||
shopDetails: ShopDetails | null;
|
shopDetails: ShopDetailsWithAssets | null;
|
||||||
repacks: GameRepack[];
|
repacks: GameRepack[];
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
gameTitle: string;
|
gameTitle: string;
|
||||||
|
|||||||
44
src/renderer/src/declaration.d.ts
vendored
44
src/renderer/src/declaration.d.ts
vendored
@@ -3,7 +3,6 @@ import type {
|
|||||||
AppUpdaterEvent,
|
AppUpdaterEvent,
|
||||||
GameShop,
|
GameShop,
|
||||||
HowLongToBeatCategory,
|
HowLongToBeatCategory,
|
||||||
ShopDetails,
|
|
||||||
Steam250Game,
|
Steam250Game,
|
||||||
DownloadProgress,
|
DownloadProgress,
|
||||||
SeedingStatus,
|
SeedingStatus,
|
||||||
@@ -33,6 +32,11 @@ import type {
|
|||||||
Badge,
|
Badge,
|
||||||
Auth,
|
Auth,
|
||||||
ShortcutLocation,
|
ShortcutLocation,
|
||||||
|
CatalogueSearchResult,
|
||||||
|
ShopAssets,
|
||||||
|
ShopDetailsWithAssets,
|
||||||
|
AchievementCustomNotificationPosition,
|
||||||
|
AchievementNotificationInfo,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
@@ -69,13 +73,18 @@ declare global {
|
|||||||
payload: CatalogueSearchPayload,
|
payload: CatalogueSearchPayload,
|
||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
) => Promise<{ edges: any[]; count: number }>;
|
) => Promise<{ edges: CatalogueSearchResult[]; count: number }>;
|
||||||
getCatalogue: (category: CatalogueCategory) => Promise<any[]>;
|
getCatalogue: (category: CatalogueCategory) => Promise<ShopAssets[]>;
|
||||||
|
saveGameShopAssets: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
assets: ShopAssets
|
||||||
|
) => Promise<void>;
|
||||||
getGameShopDetails: (
|
getGameShopDetails: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
language: string
|
language: string
|
||||||
) => Promise<ShopDetails | null>;
|
) => Promise<ShopDetailsWithAssets | null>;
|
||||||
getRandomGame: () => Promise<Steam250Game>;
|
getRandomGame: () => Promise<Steam250Game>;
|
||||||
getHowLongToBeat: (
|
getHowLongToBeat: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -130,10 +139,7 @@ declare global {
|
|||||||
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
||||||
getLibrary: () => Promise<LibraryGame[]>;
|
getLibrary: () => Promise<LibraryGame[]>;
|
||||||
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
|
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||||
openGameInstallerPath: (
|
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
shop: GameShop,
|
|
||||||
objectId: string
|
|
||||||
) => Promise<boolean>;
|
|
||||||
openGameExecutablePath: (shop: GameShop, objectId: string) => Promise<void>;
|
openGameExecutablePath: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
openGame: (
|
openGame: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
@@ -168,10 +174,11 @@ declare global {
|
|||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
|
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||||
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
|
||||||
onExtractionComplete: (
|
onExtractionComplete: (
|
||||||
cb: (shop: GameShop, objectId: string) => void
|
cb: (shop: GameShop, objectId: string) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
getDefaultWinePrefixSelectionPath: () => Promise<string | null>;
|
||||||
|
createSteamShortcut: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
putDownloadSource: (
|
putDownloadSource: (
|
||||||
@@ -314,6 +321,21 @@ declare global {
|
|||||||
|
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||||
|
onAchievementUnlocked: (
|
||||||
|
cb: (
|
||||||
|
position?: AchievementCustomNotificationPosition,
|
||||||
|
achievements?: AchievementNotificationInfo[]
|
||||||
|
) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
onCombinedAchievementsUnlocked: (
|
||||||
|
cb: (
|
||||||
|
gameCount: number,
|
||||||
|
achievementCount: number,
|
||||||
|
position: AchievementCustomNotificationPosition
|
||||||
|
) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
updateAchievementCustomNotificationWindow: () => Promise<void>;
|
||||||
|
showAchievementTestNotification: () => Promise<void>;
|
||||||
|
|
||||||
/* Themes */
|
/* Themes */
|
||||||
addCustomTheme: (theme: Theme) => Promise<void>;
|
addCustomTheme: (theme: Theme) => Promise<void>;
|
||||||
@@ -327,9 +349,7 @@ declare global {
|
|||||||
|
|
||||||
/* Editor */
|
/* Editor */
|
||||||
openEditorWindow: (themeId: string) => Promise<void>;
|
openEditorWindow: (themeId: string) => Promise<void>;
|
||||||
onCssInjected: (
|
onCustomThemeUpdated: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
cb: (cssString: string) => void
|
|
||||||
) => () => Electron.IpcRenderer;
|
|
||||||
closeEditorWindow: (themeId?: string) => Promise<void>;
|
closeEditorWindow: (themeId?: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,35 +55,32 @@ export const buildGameAchievementPath = (
|
|||||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||||
new Color(color).darken(amount).alpha(alpha).toString();
|
new Color(color).darken(amount).alpha(alpha).toString();
|
||||||
|
|
||||||
export const injectCustomCss = (css: string) => {
|
export const injectCustomCss = (
|
||||||
|
css: string,
|
||||||
|
target: HTMLElement = document.head
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const currentCustomCss = document.getElementById("custom-css");
|
target.querySelector("#custom-css")?.remove();
|
||||||
if (currentCustomCss) {
|
|
||||||
currentCustomCss.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (css.startsWith(THEME_WEB_STORE_URL)) {
|
if (css.startsWith(THEME_WEB_STORE_URL)) {
|
||||||
const link = document.createElement("link");
|
const link = document.createElement("link");
|
||||||
link.id = "custom-css";
|
link.id = "custom-css";
|
||||||
link.rel = "stylesheet";
|
link.rel = "stylesheet";
|
||||||
link.href = css;
|
link.href = css;
|
||||||
document.head.appendChild(link);
|
target.appendChild(link);
|
||||||
} else {
|
} else {
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.id = "custom-css";
|
style.id = "custom-css";
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
${css}
|
${css}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
target.appendChild(style);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("failed to inject custom css:", error);
|
console.error("failed to inject custom css:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeCustomCss = () => {
|
export const removeCustomCss = (target: HTMLElement = document.head) => {
|
||||||
const currentCustomCss = document.getElementById("custom-css");
|
target.querySelector("#custom-css")?.remove();
|
||||||
if (currentCustomCss) {
|
|
||||||
currentCustomCss.remove();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import Settings from "./pages/settings/settings";
|
|||||||
import Profile from "./pages/profile/profile";
|
import Profile from "./pages/profile/profile";
|
||||||
import Achievements from "./pages/achievements/achievements";
|
import Achievements from "./pages/achievements/achievements";
|
||||||
import ThemeEditor from "./pages/theme-editor/theme-editor";
|
import ThemeEditor from "./pages/theme-editor/theme-editor";
|
||||||
|
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
||||||
@@ -84,6 +85,10 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/theme-editor" element={<ThemeEditor />} />
|
<Route path="/theme-editor" element={<ThemeEditor />} />
|
||||||
|
<Route
|
||||||
|
path="/achievement-notification"
|
||||||
|
element={<AchievementNotification />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user