mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-25 11:51:02 +00:00
Compare commits
173 Commits
feat/custo
...
feat/ota
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b9f88702 | ||
|
|
c9fc4dfc02 | ||
|
|
0ae3e35cb4 | ||
|
|
cd136c07a6 | ||
|
|
6e243822ff | ||
|
|
12274b8c57 | ||
|
|
a8bbb76190 | ||
|
|
59d4545476 | ||
|
|
621adbb1ab | ||
|
|
d530c384c9 | ||
|
|
d8e30a3f2f | ||
|
|
5e59e1a7d1 | ||
|
|
fc541daaed | ||
|
|
53d81018e9 | ||
|
|
25758a540f | ||
|
|
e5659543ce | ||
|
|
612350ac19 | ||
|
|
b9c7f992dc | ||
|
|
97dc7653b0 | ||
|
|
330f38776f | ||
|
|
b874138641 | ||
|
|
a439095260 | ||
|
|
5ff263e8c8 | ||
|
|
864c47ee46 | ||
|
|
523e19d97a | ||
|
|
03b2c8d07f | ||
|
|
d296830533 | ||
|
|
7fc4e17547 | ||
|
|
636cf287c9 | ||
|
|
84be238988 | ||
|
|
494a2a0da5 | ||
|
|
4e912b3b8d | ||
|
|
e71211f1aa | ||
|
|
a946f3bd5a | ||
|
|
374b62983b | ||
|
|
0cd4c3ccf6 | ||
|
|
7b97663b3a | ||
|
|
68e2e2a772 | ||
|
|
39979292e2 | ||
|
|
60ae7d40fa | ||
|
|
63b6b0b44e | ||
|
|
6b6dfc7adc | ||
|
|
7c33c43d9c | ||
|
|
82c0dc0d97 | ||
|
|
1cba3f350c | ||
|
|
5d0f036de2 | ||
|
|
c2e5bc0e91 | ||
|
|
030b3b8f7c | ||
|
|
38b04ee991 | ||
|
|
5f643ecd5f | ||
|
|
366ce953d4 | ||
|
|
34aea2b0c4 | ||
|
|
dcec33ada1 | ||
|
|
5877c8c798 | ||
|
|
14204f1fbe | ||
|
|
602b2fef91 | ||
|
|
2240a8c9fb | ||
|
|
6c34a1fcc0 | ||
|
|
741f9de85c | ||
|
|
5510bb9c9e | ||
|
|
df6d9df31d | ||
|
|
1e779a32c1 | ||
|
|
b21c97ea66 | ||
|
|
9bada771df | ||
|
|
7b8f7fc070 | ||
|
|
6146a1fbf1 | ||
|
|
91fd5932da | ||
|
|
a9b67ad1e6 | ||
|
|
aa4def327a | ||
|
|
47ac8e63ac | ||
|
|
3d71dded3d | ||
|
|
055be6b10a | ||
|
|
b0d9d18c6c | ||
|
|
e6d5a2e871 | ||
|
|
063e97e0ec | ||
|
|
6667e00c91 | ||
|
|
8653e62dce | ||
|
|
71f391c8e8 | ||
|
|
1f7947f50f | ||
|
|
72562b13ef | ||
|
|
1f05dc8f78 | ||
|
|
52d3750acc | ||
|
|
b91306e70e | ||
|
|
e3fb325b7b | ||
|
|
2e68018059 | ||
|
|
f11296f3a9 | ||
|
|
a92563509b | ||
|
|
899f68318f | ||
|
|
1b5f70a075 | ||
|
|
fab02c4d16 | ||
|
|
8d5b169166 | ||
|
|
3160ee68f1 | ||
|
|
80275dc08f | ||
|
|
958e66d795 | ||
|
|
4116459577 | ||
|
|
449ea92268 | ||
|
|
f1a1270230 | ||
|
|
63fe7acd0d | ||
|
|
2beb9c469a | ||
|
|
19cf24ef48 | ||
|
|
461da55070 | ||
|
|
f08ad361ed | ||
|
|
abe2314c38 | ||
|
|
cb9b120093 | ||
|
|
3058a05ca8 | ||
|
|
aacf9abc6a | ||
|
|
79498abdb5 | ||
|
|
2aebbb8fa2 | ||
|
|
c3ce92a48e | ||
|
|
8eb15900fe | ||
|
|
9812f455c9 | ||
|
|
74983520ed | ||
|
|
e3cd596fb2 | ||
|
|
37db88f48f | ||
|
|
0e999496e3 | ||
|
|
9c87964e16 | ||
|
|
45903d778e | ||
|
|
937a3d189e | ||
|
|
406b455960 | ||
|
|
c39f8c703b | ||
|
|
01b8f0c3af | ||
|
|
5930132de4 | ||
|
|
776859c58e | ||
|
|
cb0fc82644 | ||
|
|
3826294337 | ||
|
|
a625541125 | ||
|
|
300cff2be6 | ||
|
|
3c502679a6 | ||
|
|
701226d25d | ||
|
|
4f5c345c42 | ||
|
|
d513377f1c | ||
|
|
46491af539 | ||
|
|
f9f110bd1c | ||
|
|
e14c125a43 | ||
|
|
d8947e5ab8 | ||
|
|
5648e393bf | ||
|
|
d7e47323e5 | ||
|
|
30e4d694cd | ||
|
|
0dbf904bb8 | ||
|
|
e678cc3801 | ||
|
|
d7d920a7b7 | ||
|
|
3afd30765f | ||
|
|
5bf5ca3504 | ||
|
|
6e2d7f5c8a | ||
|
|
a003153239 | ||
|
|
40aaa5d3ca | ||
|
|
0e2cc2b25c | ||
|
|
6f258aecee | ||
|
|
35ce07c5f4 | ||
|
|
7514177423 | ||
|
|
4f3113002b | ||
|
|
e43f80b830 | ||
|
|
6c34e5516f | ||
|
|
81f001ade4 | ||
|
|
688ae215d7 | ||
|
|
6f5baf0df5 | ||
|
|
1d5be940f9 | ||
|
|
cd25fa715e | ||
|
|
13a2a8161c | ||
|
|
7ba65b3357 | ||
|
|
bd56c48cb3 | ||
|
|
1525ccc438 | ||
|
|
08702d3157 | ||
|
|
8dba05eafe | ||
|
|
5e217bf086 | ||
|
|
349da9536a | ||
|
|
dad493f558 | ||
|
|
9939b68fec | ||
|
|
d00fe8dedc | ||
|
|
f9673b1a5a | ||
|
|
3ff8ed735e | ||
|
|
ca6331e45b | ||
|
|
0d19328798 |
40
.github/workflows/build-renderer.yml
vendored
Normal file
40
.github/workflows/build-renderer.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Build Renderer
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.18.0
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
- name: Build Renderer
|
||||||
|
run: yarn build
|
||||||
|
env:
|
||||||
|
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
run: |
|
||||||
|
npx --yes wrangler@3 pages deploy out/renderer \
|
||||||
|
--project-name="hydra" \
|
||||||
|
--commit-dirty=true \
|
||||||
|
--branch="main"
|
||||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, ubuntu-latest]
|
os: [windows-2022, ubuntu-latest]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.18.0
|
node-version: 20.18.3
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||||
|
|
||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-2022'
|
||||||
run: yarn build:win
|
run: yarn build:win
|
||||||
env:
|
env:
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.18.0
|
node-version: 20.18.3
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
|
|||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -6,13 +6,13 @@ concurrency:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: main
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, ubuntu-latest]
|
os: [windows-2022, ubuntu-latest]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.18.0
|
node-version: 20.18.3
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
@@ -57,9 +57,10 @@ jobs:
|
|||||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||||
|
MAIN_VITE_RENDERER_URL: ${{ vars.MAIN_VITE_RENDERER_URL }}
|
||||||
|
|
||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-2022'
|
||||||
run: yarn build:win
|
run: yarn build:win
|
||||||
env:
|
env:
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
@@ -73,6 +74,7 @@ jobs:
|
|||||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||||
|
MAIN_VITE_RENDERER_URL: ${{ vars.MAIN_VITE_RENDERER_URL }}
|
||||||
|
|
||||||
- name: Create artifact
|
- name: Create artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
32
package.json
32
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.6.8",
|
"version": "3.7.0",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
@@ -32,17 +32,24 @@
|
|||||||
"protoc": "npx protoc --ts_out src/main/generated --proto_path proto proto/*.proto"
|
"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.2",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@fontsource/noto-sans": "^5.1.0",
|
"@fontsource/noto-sans": "^5.2.10",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
|
"@tiptap/extension-bold": "^3.6.2",
|
||||||
|
"@tiptap/extension-italic": "^3.6.2",
|
||||||
|
"@tiptap/extension-link": "^3.6.2",
|
||||||
|
"@tiptap/extension-underline": "^3.6.2",
|
||||||
|
"@tiptap/react": "^3.6.2",
|
||||||
|
"@tiptap/starter-kit": "^3.6.2",
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.12.2",
|
||||||
"axios-cookiejar-support": "^5.0.5",
|
"axios-cookiejar-support": "^5.0.5",
|
||||||
|
"check-disk-space": "^3.4.0",
|
||||||
"classic-level": "^2.0.0",
|
"classic-level": "^2.0.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
@@ -51,8 +58,7 @@
|
|||||||
"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",
|
||||||
"diskusage": "^1.2.0",
|
"electron-log": "^5.4.3",
|
||||||
"electron-log": "^5.2.4",
|
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"embla-carousel-autoplay": "^8.6.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
@@ -63,6 +69,7 @@
|
|||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
"parse-torrent": "^11.0.18",
|
"parse-torrent": "^11.0.18",
|
||||||
"rc-virtual-list": "^3.18.3",
|
"rc-virtual-list": "^3.18.3",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
@@ -80,6 +87,7 @@
|
|||||||
"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",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"winreg": "^1.2.5",
|
"winreg": "^1.2.5",
|
||||||
"ws": "^8.18.1",
|
"ws": "^8.18.1",
|
||||||
"yaml": "^2.6.1",
|
"yaml": "^2.6.1",
|
||||||
@@ -109,7 +117,7 @@
|
|||||||
"@types/winreg": "^1.2.36",
|
"@types/winreg": "^1.2.36",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^32.3.3",
|
"electron": "^33.4.11",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^3.0.0",
|
"electron-vite": "^3.0.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
@@ -123,8 +131,8 @@
|
|||||||
"sass-embedded": "^1.80.6",
|
"sass-embedded": "^1.80.6",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.0.12",
|
"vite": "5.4.20",
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
"vite-plugin-svgr": "^4.5.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
151
python_rpc/http_multi_link_downloader.py
Normal file
151
python_rpc/http_multi_link_downloader.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import aria2p
|
||||||
|
from aria2p.client import ClientException as DownloadNotFound
|
||||||
|
|
||||||
|
class HttpMultiLinkDownloader:
|
||||||
|
def __init__(self):
|
||||||
|
self.downloads = []
|
||||||
|
self.completed_downloads = []
|
||||||
|
self.total_size = None
|
||||||
|
self.aria2 = aria2p.API(
|
||||||
|
aria2p.Client(
|
||||||
|
host="http://localhost",
|
||||||
|
port=6800,
|
||||||
|
secret=""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_download(self, urls: list[str], save_path: str, header: str = None, out: str = None, total_size: int = None):
|
||||||
|
"""Add multiple URLs to download queue with same options"""
|
||||||
|
options = {"dir": save_path}
|
||||||
|
if header:
|
||||||
|
options["header"] = header
|
||||||
|
if out:
|
||||||
|
options["out"] = out
|
||||||
|
|
||||||
|
# Clear any existing downloads first
|
||||||
|
self.cancel_download()
|
||||||
|
self.completed_downloads = []
|
||||||
|
self.total_size = total_size
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
try:
|
||||||
|
added_downloads = self.aria2.add(url, options=options)
|
||||||
|
self.downloads.extend(added_downloads)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding download for URL {url}: {str(e)}")
|
||||||
|
|
||||||
|
def pause_download(self):
|
||||||
|
"""Pause all active downloads"""
|
||||||
|
if self.downloads:
|
||||||
|
try:
|
||||||
|
self.aria2.pause(self.downloads)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error pausing downloads: {str(e)}")
|
||||||
|
|
||||||
|
def cancel_download(self):
|
||||||
|
"""Cancel and remove all downloads"""
|
||||||
|
if self.downloads:
|
||||||
|
try:
|
||||||
|
# First try to stop the downloads
|
||||||
|
self.aria2.remove(self.downloads)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing downloads: {str(e)}")
|
||||||
|
finally:
|
||||||
|
# Clear the downloads list regardless of success/failure
|
||||||
|
self.downloads = []
|
||||||
|
self.completed_downloads = []
|
||||||
|
|
||||||
|
def get_download_status(self):
|
||||||
|
"""Get status for all tracked downloads, auto-remove completed/failed ones"""
|
||||||
|
if not self.downloads and not self.completed_downloads:
|
||||||
|
return []
|
||||||
|
|
||||||
|
total_completed = 0
|
||||||
|
current_download_speed = 0
|
||||||
|
active_downloads = []
|
||||||
|
to_remove = []
|
||||||
|
|
||||||
|
# First calculate sizes from completed downloads
|
||||||
|
for completed in self.completed_downloads:
|
||||||
|
total_completed += completed['size']
|
||||||
|
|
||||||
|
# Then check active downloads
|
||||||
|
for download in self.downloads:
|
||||||
|
try:
|
||||||
|
current_download = self.aria2.get_download(download.gid)
|
||||||
|
|
||||||
|
# Skip downloads that are not properly initialized
|
||||||
|
if not current_download or not current_download.files:
|
||||||
|
to_remove.append(download)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add to completed size and speed calculations
|
||||||
|
total_completed += current_download.completed_length
|
||||||
|
current_download_speed += current_download.download_speed
|
||||||
|
|
||||||
|
# If download is complete, move it to completed_downloads
|
||||||
|
if current_download.status == 'complete':
|
||||||
|
self.completed_downloads.append({
|
||||||
|
'name': current_download.name,
|
||||||
|
'size': current_download.total_length
|
||||||
|
})
|
||||||
|
to_remove.append(download)
|
||||||
|
else:
|
||||||
|
active_downloads.append({
|
||||||
|
'name': current_download.name,
|
||||||
|
'size': current_download.total_length,
|
||||||
|
'completed': current_download.completed_length,
|
||||||
|
'speed': current_download.download_speed
|
||||||
|
})
|
||||||
|
|
||||||
|
except DownloadNotFound:
|
||||||
|
to_remove.append(download)
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting download status: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Clean up completed/removed downloads from active list
|
||||||
|
for download in to_remove:
|
||||||
|
try:
|
||||||
|
if download in self.downloads:
|
||||||
|
self.downloads.remove(download)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Return aggregate status
|
||||||
|
if self.total_size or active_downloads or self.completed_downloads:
|
||||||
|
# Use the first active download's name as the folder name, or completed if none active
|
||||||
|
folder_name = None
|
||||||
|
if active_downloads:
|
||||||
|
folder_name = active_downloads[0]['name']
|
||||||
|
elif self.completed_downloads:
|
||||||
|
folder_name = self.completed_downloads[0]['name']
|
||||||
|
|
||||||
|
if folder_name and '/' in folder_name:
|
||||||
|
folder_name = folder_name.split('/')[0]
|
||||||
|
|
||||||
|
# Use provided total size if available, otherwise sum from downloads
|
||||||
|
total_size = self.total_size
|
||||||
|
if not total_size:
|
||||||
|
total_size = sum(d['size'] for d in active_downloads) + sum(d['size'] for d in self.completed_downloads)
|
||||||
|
|
||||||
|
# Calculate completion status based on total downloaded vs total size
|
||||||
|
is_complete = len(active_downloads) == 0 and total_completed >= (total_size * 0.99) # Allow 1% margin for size differences
|
||||||
|
|
||||||
|
# If all downloads are complete, clear the completed_downloads list to prevent status updates
|
||||||
|
if is_complete:
|
||||||
|
self.completed_downloads = []
|
||||||
|
|
||||||
|
return [{
|
||||||
|
'folderName': folder_name,
|
||||||
|
'fileSize': total_size,
|
||||||
|
'progress': total_completed / total_size if total_size > 0 else 0,
|
||||||
|
'downloadSpeed': current_download_speed,
|
||||||
|
'numPeers': 0,
|
||||||
|
'numSeeds': 0,
|
||||||
|
'status': 'complete' if is_complete else 'active',
|
||||||
|
'bytesDownloaded': total_completed,
|
||||||
|
}]
|
||||||
|
|
||||||
|
return []
|
||||||
@@ -3,6 +3,7 @@ import sys, json, urllib.parse, psutil
|
|||||||
from torrent_downloader import TorrentDownloader
|
from torrent_downloader import TorrentDownloader
|
||||||
from http_downloader import HttpDownloader
|
from http_downloader import HttpDownloader
|
||||||
from profile_image_processor import ProfileImageProcessor
|
from profile_image_processor import ProfileImageProcessor
|
||||||
|
from http_multi_link_downloader import HttpMultiLinkDownloader
|
||||||
import libtorrent as lt
|
import libtorrent as lt
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -24,7 +25,15 @@ if start_download_payload:
|
|||||||
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
||||||
downloading_game_id = initial_download['game_id']
|
downloading_game_id = initial_download['game_id']
|
||||||
|
|
||||||
if initial_download['url'].startswith('magnet'):
|
if isinstance(initial_download['url'], list):
|
||||||
|
# Handle multiple URLs using HttpMultiLinkDownloader
|
||||||
|
http_multi_downloader = HttpMultiLinkDownloader()
|
||||||
|
downloads[initial_download['game_id']] = http_multi_downloader
|
||||||
|
try:
|
||||||
|
http_multi_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
|
||||||
|
except Exception as e:
|
||||||
|
print("Error starting multi-link download", e)
|
||||||
|
elif initial_download['url'].startswith('magnet'):
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[initial_download['game_id']] = torrent_downloader
|
downloads[initial_download['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
@@ -62,12 +71,23 @@ def status():
|
|||||||
return auth_error
|
return auth_error
|
||||||
|
|
||||||
downloader = downloads.get(downloading_game_id)
|
downloader = downloads.get(downloading_game_id)
|
||||||
if downloader:
|
if not downloader:
|
||||||
status = downloads.get(downloading_game_id).get_download_status()
|
|
||||||
return jsonify(status), 200
|
|
||||||
else:
|
|
||||||
return jsonify(None)
|
return jsonify(None)
|
||||||
|
|
||||||
|
status = downloader.get_download_status()
|
||||||
|
if not status:
|
||||||
|
return jsonify(None)
|
||||||
|
|
||||||
|
if isinstance(status, list):
|
||||||
|
if not status: # Empty list
|
||||||
|
return jsonify(None)
|
||||||
|
|
||||||
|
# For multi-link downloader, use the aggregated status
|
||||||
|
# The status will already be aggregated by the HttpMultiLinkDownloader
|
||||||
|
return jsonify(status[0]), 200
|
||||||
|
|
||||||
|
return jsonify(status), 200
|
||||||
|
|
||||||
@app.route("/seed-status", methods=["GET"])
|
@app.route("/seed-status", methods=["GET"])
|
||||||
def seed_status():
|
def seed_status():
|
||||||
auth_error = validate_rpc_password()
|
auth_error = validate_rpc_password()
|
||||||
@@ -81,10 +101,24 @@ def seed_status():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
response = downloader.get_download_status()
|
response = downloader.get_download_status()
|
||||||
if response is None:
|
if not response:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.get('status') == 5:
|
if isinstance(response, list):
|
||||||
|
# For multi-link downloader, check if all files are complete
|
||||||
|
if response and all(item['status'] == 'complete' for item in response):
|
||||||
|
seed_status.append({
|
||||||
|
'gameId': game_id,
|
||||||
|
'status': 'complete',
|
||||||
|
'folderName': response[0]['folderName'],
|
||||||
|
'fileSize': sum(item['fileSize'] for item in response),
|
||||||
|
'bytesDownloaded': sum(item['bytesDownloaded'] for item in response),
|
||||||
|
'downloadSpeed': 0,
|
||||||
|
'numPeers': 0,
|
||||||
|
'numSeeds': 0,
|
||||||
|
'progress': 1.0
|
||||||
|
})
|
||||||
|
elif response.get('status') == 5: # Original torrent seeding check
|
||||||
seed_status.append({
|
seed_status.append({
|
||||||
'gameId': game_id,
|
'gameId': game_id,
|
||||||
**response,
|
**response,
|
||||||
@@ -143,7 +177,15 @@ def action():
|
|||||||
|
|
||||||
existing_downloader = downloads.get(game_id)
|
existing_downloader = downloads.get(game_id)
|
||||||
|
|
||||||
if url.startswith('magnet'):
|
if isinstance(url, list):
|
||||||
|
# Handle multiple URLs using HttpMultiLinkDownloader
|
||||||
|
if existing_downloader and isinstance(existing_downloader, HttpMultiLinkDownloader):
|
||||||
|
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
|
else:
|
||||||
|
http_multi_downloader = HttpMultiLinkDownloader()
|
||||||
|
downloads[game_id] = http_multi_downloader
|
||||||
|
http_multi_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
|
elif url.startswith('magnet'):
|
||||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'])
|
existing_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -70,7 +70,25 @@
|
|||||||
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
|
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
|
||||||
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
|
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
|
||||||
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
|
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
|
||||||
"edit_game_modal_assets": "Assets"
|
"edit_game_modal_assets": "Assets",
|
||||||
|
"edit_game_modal_drop_icon_image_here": "Drop icon image here",
|
||||||
|
"edit_game_modal_drop_logo_image_here": "Drop logo image here",
|
||||||
|
"edit_game_modal_drop_hero_image_here": "Drop hero image here",
|
||||||
|
"edit_game_modal_drop_to_replace_icon": "Drop to replace icon",
|
||||||
|
"edit_game_modal_drop_to_replace_logo": "Drop to replace logo",
|
||||||
|
"edit_game_modal_drop_to_replace_hero": "Drop to replace hero",
|
||||||
|
"install_decky_plugin": "Install Decky Plugin",
|
||||||
|
"update_decky_plugin": "Update Decky Plugin",
|
||||||
|
"decky_plugin_installed_version": "Decky Plugin (v{{version}})",
|
||||||
|
"install_decky_plugin_title": "Install Hydra Decky Plugin",
|
||||||
|
"install_decky_plugin_message": "This will download and install the Hydra plugin for Decky Loader. This may require elevated permissions. Continue?",
|
||||||
|
"update_decky_plugin_title": "Update Hydra Decky Plugin",
|
||||||
|
"update_decky_plugin_message": "A new version of the Hydra Decky plugin is available. Would you like to update it now?",
|
||||||
|
"decky_plugin_installed": "Decky plugin v{{version}} installed successfully",
|
||||||
|
"decky_plugin_installation_failed": "Failed to install Decky plugin: {{error}}",
|
||||||
|
"decky_plugin_installation_error": "Error installing Decky plugin: {{error}}",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
@@ -160,11 +178,13 @@
|
|||||||
"open_folder": "Open folder",
|
"open_folder": "Open folder",
|
||||||
"open_download_location": "See downloaded files",
|
"open_download_location": "See downloaded files",
|
||||||
"create_shortcut": "Create desktop shortcut",
|
"create_shortcut": "Create desktop shortcut",
|
||||||
|
"create_shortcut_simple": "Create shortcut",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"remove_files": "Remove files",
|
"remove_files": "Remove files",
|
||||||
"remove_from_library_title": "Are you sure?",
|
"remove_from_library_title": "Are you sure?",
|
||||||
"remove_from_library_description": "This will remove {{game}} from your library",
|
"remove_from_library_description": "This will remove {{game}} from your library",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
|
"properties": "Properties",
|
||||||
"executable_section_title": "Executable",
|
"executable_section_title": "Executable",
|
||||||
"executable_section_description": "Path of the file that will be executed when \"Play\" is clicked",
|
"executable_section_description": "Path of the file that will be executed when \"Play\" is clicked",
|
||||||
"downloads_section_title": "Downloads",
|
"downloads_section_title": "Downloads",
|
||||||
@@ -178,6 +198,13 @@
|
|||||||
"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",
|
"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",
|
||||||
|
"add_to_favorites": "Add to favorites",
|
||||||
|
"remove_from_favorites": "Remove from favorites",
|
||||||
|
"failed_update_favorites": "Failed to update favorites",
|
||||||
|
"game_removed_from_library": "Game removed from library",
|
||||||
|
"failed_remove_from_library": "Failed to remove from library",
|
||||||
|
"files_removed_success": "Files removed successfully",
|
||||||
|
"failed_remove_files": "Failed to remove files",
|
||||||
"nsfw_content_title": "This game contains inappropriate 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",
|
||||||
@@ -185,6 +212,7 @@
|
|||||||
"stats": "Stats",
|
"stats": "Stats",
|
||||||
"download_count": "Downloads",
|
"download_count": "Downloads",
|
||||||
"player_count": "Active players",
|
"player_count": "Active players",
|
||||||
|
"rating_count": "Rating",
|
||||||
"download_error": "This download option is not available",
|
"download_error": "This download option is not available",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"executable_path_in_use": "Executable already in use by \"{{game}}\"",
|
"executable_path_in_use": "Executable already in use by \"{{game}}\"",
|
||||||
@@ -192,6 +220,39 @@
|
|||||||
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.",
|
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.",
|
||||||
"achievements": "Achievements",
|
"achievements": "Achievements",
|
||||||
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
|
||||||
|
"show_more": "Show more",
|
||||||
|
"show_less": "Show less",
|
||||||
|
"reviews": "Reviews",
|
||||||
|
"leave_a_review": "Leave a Review",
|
||||||
|
"write_review_placeholder": "Share your thoughts about this game...",
|
||||||
|
"sort_newest": "Newest",
|
||||||
|
"no_reviews_yet": "No reviews yet",
|
||||||
|
"be_first_to_review": "Be the first to share your thoughts about this game!",
|
||||||
|
"sort_oldest": "Oldest",
|
||||||
|
"sort_highest_score": "Highest Score",
|
||||||
|
"sort_lowest_score": "Lowest Score",
|
||||||
|
"sort_most_voted": "Most Voted",
|
||||||
|
"rating": "Rating",
|
||||||
|
"rating_stats": "Rating",
|
||||||
|
"rating_very_negative": "Very Negative",
|
||||||
|
"rating_negative": "Negative",
|
||||||
|
"rating_neutral": "Neutral",
|
||||||
|
"rating_positive": "Positive",
|
||||||
|
"rating_very_positive": "Very Positive",
|
||||||
|
"submit_review": "Submit",
|
||||||
|
"submitting": "Submitting...",
|
||||||
|
"review_submitted_successfully": "Review submitted successfully!",
|
||||||
|
"review_submission_failed": "Failed to submit review. Please try again.",
|
||||||
|
"review_cannot_be_empty": "Review text field cannot be empty.",
|
||||||
|
"review_deleted_successfully": "Review deleted successfully.",
|
||||||
|
"review_deletion_failed": "Failed to delete review. Please try again.",
|
||||||
|
"loading_reviews": "Loading reviews...",
|
||||||
|
"loading_more_reviews": "Loading more reviews...",
|
||||||
|
"load_more_reviews": "Load More Reviews",
|
||||||
|
"you_seemed_to_enjoy_this_game": "You've seemed to enjoy this game",
|
||||||
|
"would_you_recommend_this_game": "Would you like to leave a review to this game?",
|
||||||
|
"yes": "Yes",
|
||||||
|
"maybe_later": "Maybe Later",
|
||||||
"cloud_save": "Cloud save",
|
"cloud_save": "Cloud save",
|
||||||
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
@@ -204,6 +265,7 @@
|
|||||||
"uploading_backup": "Uploading backup…",
|
"uploading_backup": "Uploading backup…",
|
||||||
"no_backups": "You haven't created any backups for this game yet",
|
"no_backups": "You haven't created any backups for this game yet",
|
||||||
"backup_uploaded": "Backup uploaded",
|
"backup_uploaded": "Backup uploaded",
|
||||||
|
"backup_failed": "Backup failed",
|
||||||
"backup_deleted": "Backup deleted",
|
"backup_deleted": "Backup deleted",
|
||||||
"backup_restored": "Backup restored",
|
"backup_restored": "Backup restored",
|
||||||
"see_all_achievements": "See all achievements",
|
"see_all_achievements": "See all achievements",
|
||||||
@@ -286,7 +348,16 @@
|
|||||||
"historical_keyshop": "Historical keyshop",
|
"historical_keyshop": "Historical keyshop",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"caption": "Caption",
|
"caption": "Caption",
|
||||||
"audio": "Audio"
|
"audio": "Audio",
|
||||||
|
"filter_by_source": "Filter by source",
|
||||||
|
"no_repacks_found": "No sources found for this game",
|
||||||
|
"delete_review": "Delete review",
|
||||||
|
"remove_review": "Remove Review",
|
||||||
|
"delete_review_modal_title": "Are you sure you want to delete your review?",
|
||||||
|
"delete_review_modal_description": "This action cannot be undone.",
|
||||||
|
"delete_review_modal_delete_button": "Delete",
|
||||||
|
"delete_review_modal_cancel_button": "Cancel",
|
||||||
|
"vote_failed": "Failed to register your vote. Please try again."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
@@ -324,6 +395,7 @@
|
|||||||
"stop_seeding": "Stop seeding",
|
"stop_seeding": "Stop seeding",
|
||||||
"resume_seeding": "Resume seeding",
|
"resume_seeding": "Resume seeding",
|
||||||
"options": "Manage",
|
"options": "Manage",
|
||||||
|
"alldebrid_size_not_supported": "Download info for AllDebrid is not supported yet",
|
||||||
"extract": "Extract files",
|
"extract": "Extract files",
|
||||||
"extracting": "Extracting files…"
|
"extracting": "Extracting files…"
|
||||||
},
|
},
|
||||||
@@ -427,12 +499,25 @@
|
|||||||
"delete_theme_description": "This will delete the theme {{theme}}",
|
"delete_theme_description": "This will delete the theme {{theme}}",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
|
"debrid": "Debrid",
|
||||||
|
"debrid_description": "Debrid services are premium unrestricted downloaders that allow you to quickly download files hosted on various file hosting services, only limited by your internet speed.",
|
||||||
"enable_torbox": "Enable TorBox",
|
"enable_torbox": "Enable TorBox",
|
||||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||||
"torbox_account_linked": "TorBox account linked",
|
"torbox_account_linked": "TorBox account linked",
|
||||||
"create_real_debrid_account": "Click here if you don't have a Real-Debrid account yet",
|
"create_real_debrid_account": "Click here if you don't have a Real-Debrid account yet",
|
||||||
"create_torbox_account": "Click here if you don't have a TorBox account yet",
|
"create_torbox_account": "Click here if you don't have a TorBox account yet",
|
||||||
"real_debrid_account_linked": "Real-Debrid account linked",
|
"real_debrid_account_linked": "Real-Debrid account linked",
|
||||||
|
"enable_all_debrid": "Enable All-Debrid",
|
||||||
|
"all_debrid_description": "All-Debrid is an unrestricted downloader that allows you to quickly download files from various sources.",
|
||||||
|
"all_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to All-Debrid",
|
||||||
|
"all_debrid_account_linked": "All-Debrid account linked successfully",
|
||||||
|
"alldebrid_missing_key": "Please provide an API key",
|
||||||
|
"alldebrid_invalid_key": "Invalid API key",
|
||||||
|
"alldebrid_blocked": "Your API key is geo-blocked or IP-blocked",
|
||||||
|
"alldebrid_banned": "This account has been banned",
|
||||||
|
"alldebrid_unknown_error": "An unknown error occurred",
|
||||||
|
"alldebrid_invalid_response": "Invalid response from All-Debrid",
|
||||||
|
"alldebrid_network_error": "Network error. Please check your connection",
|
||||||
"name_min_length": "Theme name must be at least 3 characters long",
|
"name_min_length": "Theme name must be at least 3 characters long",
|
||||||
"import_theme": "Import theme",
|
"import_theme": "Import theme",
|
||||||
"import_theme_description": "You will import {{theme}} from the theme store",
|
"import_theme_description": "You will import {{theme}} from the theme store",
|
||||||
@@ -490,7 +575,8 @@
|
|||||||
"game_card": {
|
"game_card": {
|
||||||
"available_one": "Available",
|
"available_one": "Available",
|
||||||
"available_other": "Available",
|
"available_other": "Available",
|
||||||
"no_downloads": "No downloads available"
|
"no_downloads": "No downloads available",
|
||||||
|
"calculating": "Calculating"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Programs not installed",
|
"title": "Programs not installed",
|
||||||
@@ -593,7 +679,10 @@
|
|||||||
"error_adding_friend": "Could not send friend request. Please check friend code",
|
"error_adding_friend": "Could not send friend request. Please check friend code",
|
||||||
"friend_code_length_error": "Friend code must have 8 characters",
|
"friend_code_length_error": "Friend code must have 8 characters",
|
||||||
"game_removed_from_pinned": "Game removed from pinned",
|
"game_removed_from_pinned": "Game removed from pinned",
|
||||||
"game_added_to_pinned": "Game added to pinned"
|
"game_added_to_pinned": "Game added to pinned",
|
||||||
|
"karma": "Karma",
|
||||||
|
"karma_count": "karma",
|
||||||
|
"karma_description": "Earned from positive likes on reviews"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked",
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
|
|||||||
@@ -204,6 +204,7 @@
|
|||||||
"uploading_backup": "Subiendo copia de seguridad…",
|
"uploading_backup": "Subiendo copia de seguridad…",
|
||||||
"no_backups": "No has creado ninguna copia de seguridad para este juego todavía",
|
"no_backups": "No has creado ninguna copia de seguridad para este juego todavía",
|
||||||
"backup_uploaded": "Copia de seguridad subida",
|
"backup_uploaded": "Copia de seguridad subida",
|
||||||
|
"backup_failed": "Copia de seguridad fallida",
|
||||||
"backup_deleted": "Copia de seguridad eliminada",
|
"backup_deleted": "Copia de seguridad eliminada",
|
||||||
"backup_restored": "Copia de seguridad restaurada",
|
"backup_restored": "Copia de seguridad restaurada",
|
||||||
"see_all_achievements": "Ver todos los logros",
|
"see_all_achievements": "Ver todos los logros",
|
||||||
|
|||||||
@@ -1,40 +1,131 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Magyar",
|
"language_name": "Magyar",
|
||||||
|
"app": {
|
||||||
|
"successfully_signed_in": "Sikeresen bejelentkeztél"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"surprise_me": "Lepj meg",
|
"surprise_me": "Lepj meg",
|
||||||
"no_results": "Nem található"
|
"no_results": "Nincs találat",
|
||||||
|
"start_typing": "Kereséshez gépelj...",
|
||||||
|
"hot": "Most felkapott",
|
||||||
|
"weekly": "📅 A hét felkapott játékai",
|
||||||
|
"achievements": "🏆 Achievement támogatott"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Katalógus",
|
"catalogue": "Katalógus",
|
||||||
"downloads": "Letöltések",
|
"downloads": "Letöltések",
|
||||||
"settings": "Beállítások",
|
"settings": "Beállítások",
|
||||||
"my_library": "Könyvtáram",
|
"my_library": "Könyvtáram",
|
||||||
"downloading_metadata": "{{title}} (Metadata letöltése…)",
|
"downloading_metadata": "{{title}} (metaadatai letöltése…)",
|
||||||
"paused": "{{title}} (Szünet)",
|
"paused": "{{title}} (Szüneteltetve)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||||
"filter": "Könyvtár szűrése",
|
"filter": "Könyvtár szűrése",
|
||||||
"home": "Főoldal",
|
"home": "Főoldal",
|
||||||
"favorites": "Kedvenc játékok"
|
"queued": "A(z) {{title}} (Várakozósorban van)",
|
||||||
|
"game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl",
|
||||||
|
"sign_in": "Bejelentkezés",
|
||||||
|
"friends": "Barátok",
|
||||||
|
"need_help": "Elakadtál?",
|
||||||
|
"favorites": "Kedvenc játékok",
|
||||||
|
"playable_button_title": "Csak az azonnal játszható játékokat mutasd",
|
||||||
|
"add_custom_game_tooltip": "Saját játék hozzáadása",
|
||||||
|
"show_playable_only_tooltip": "Csak játszható játék mutatása",
|
||||||
|
"custom_game_modal": "Saját játék hozzáadása:",
|
||||||
|
"custom_game_modal_description": "Adj meg egy futtatható fájlt",
|
||||||
|
"custom_game_modal_executable_path": "Futtatható fájl",
|
||||||
|
"custom_game_modal_select_executable": "Fájl útvonala",
|
||||||
|
"custom_game_modal_title": "Játékcím",
|
||||||
|
"custom_game_modal_enter_title": "Játék elnevezése",
|
||||||
|
"custom_game_modal_browse": "Tallózás",
|
||||||
|
"custom_game_modal_cancel": "Mégse",
|
||||||
|
"custom_game_modal_add": "Játék hozzáadása",
|
||||||
|
"custom_game_modal_adding": "Játék hozzáadása...",
|
||||||
|
"custom_game_modal_success": "Saját játék sikeresen hozzáadva",
|
||||||
|
"custom_game_modal_failed": "Saját játék hozzáadása sikertelen",
|
||||||
|
"custom_game_modal_executable": "Futtatható fájl",
|
||||||
|
"edit_game_modal": "Játékmegjelenítése:",
|
||||||
|
"edit_game_modal_description": "Játékcím és vizuális elemek módosítása",
|
||||||
|
"edit_game_modal_title": "Játékcím",
|
||||||
|
"edit_game_modal_enter_title": "Játék elnevezése",
|
||||||
|
"edit_game_modal_image": "Kép",
|
||||||
|
"edit_game_modal_select_image": "Kép útvonala",
|
||||||
|
"edit_game_modal_browse": "Tallózás",
|
||||||
|
"edit_game_modal_image_preview": "Kép előnézete",
|
||||||
|
"edit_game_modal_icon": "Ikon",
|
||||||
|
"edit_game_modal_select_icon": "Ikon útvonala",
|
||||||
|
"edit_game_modal_icon_preview": "Ikon előnézete",
|
||||||
|
"edit_game_modal_logo": "Logó",
|
||||||
|
"edit_game_modal_select_logo": "Logó útvonala",
|
||||||
|
"edit_game_modal_logo_preview": "Logó előnézete",
|
||||||
|
"edit_game_modal_hero": "Borítókép",
|
||||||
|
"edit_game_modal_select_hero": "Borítókép útvonala",
|
||||||
|
"edit_game_modal_hero_preview": "Borítókép előnézete",
|
||||||
|
"edit_game_modal_cancel": "Mégse",
|
||||||
|
"edit_game_modal_update": "Frissít",
|
||||||
|
"edit_game_modal_updating": "Frissítés...",
|
||||||
|
"edit_game_modal_fill_required": "Kérlek töltsd ki az összes kötelező mezőt",
|
||||||
|
"edit_game_modal_success": "Játék megjelenés frissítése sikeres",
|
||||||
|
"edit_game_modal_failed": "Játék megjelenés frissítése sikertelen",
|
||||||
|
"edit_game_modal_image_filter": "Kép",
|
||||||
|
"edit_game_modal_icon_resolution": "Ajánlott felbontás: 256x256px",
|
||||||
|
"edit_game_modal_logo_resolution": "Ajánlott felbontás: 640x360px",
|
||||||
|
"edit_game_modal_hero_resolution": "Ajánlott felbontás: 1920x620px",
|
||||||
|
"edit_game_modal_assets": "Vizuális elemek:",
|
||||||
|
"edit_game_modal_drop_icon_image_here": "Húzd ide az ikon képét",
|
||||||
|
"edit_game_modal_drop_logo_image_here": "Húzd ide a logó képét",
|
||||||
|
"edit_game_modal_drop_hero_image_here": "Húzd ide a borítókép képét",
|
||||||
|
"edit_game_modal_drop_to_replace_icon": "Ikon kicserélése ráhúzással",
|
||||||
|
"edit_game_modal_drop_to_replace_logo": "Logó kicserélése ráhúzással",
|
||||||
|
"edit_game_modal_drop_to_replace_hero": "Borítókép kicserélése ráhúzással",
|
||||||
|
"install_decky_plugin": "Decky Plugin Telepítése",
|
||||||
|
"update_decky_plugin": "Decky Plugin Frissítése",
|
||||||
|
"decky_plugin_installed_version": "Decky Plugin (v{{version}})",
|
||||||
|
"install_decky_plugin_title": "Telepítsd a Hydra Decky Plugint",
|
||||||
|
"install_decky_plugin_message": "Ez letölti és telepíteni fogja a Hydra plugint a Decky Loaderhez. Előfordulhat, hogy rendszergazdai jogosultságra lesz szükség. Folytatod?",
|
||||||
|
"update_decky_plugin_title": "Hydra Decky Plugin Frissítése",
|
||||||
|
"update_decky_plugin_message": "Egy új verzió elérhető a Hydra Decky Pluginhoz. Szeretnéd frissíteni?",
|
||||||
|
"decky_plugin_installed": "Decky plugin v{{version}} sikeresen telepítve",
|
||||||
|
"decky_plugin_installation_failed": "Decky plugin telepítése sikertelen: {{error}}",
|
||||||
|
"decky_plugin_installation_error": "Decky plugin telepítése hibával járt el: {{error}}",
|
||||||
|
"confirm": "Megerősít",
|
||||||
|
"cancel": "Mégse"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
"home": "Főoldal",
|
"home": "Főoldal",
|
||||||
"catalogue": "Katalógus",
|
"catalogue": "Katalógus",
|
||||||
"downloads": "Letöltések",
|
"downloads": "Letöltések",
|
||||||
"search_results": "Keresési eredmények",
|
"search_results": "Keresési találatok",
|
||||||
"settings": "Beállítások"
|
"settings": "Beállítások",
|
||||||
|
"version_available_install": "A(z) {{version}} verzió elérhető. Kattints ide az újraindításhoz és telepítéshez.",
|
||||||
|
"version_available_download": "A(z) {{version}} verzió elérhető. A letöltéshez kattints ide."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
|
"no_downloads_in_progress": "Nincs folyamatban lévő letöltés",
|
||||||
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
||||||
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}",
|
||||||
|
"calculating_eta": "{{title}} letöltése… ({{percentage}} kész) - Hátralévő idő…",
|
||||||
|
"checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)",
|
||||||
|
"installing_common_redist": "{{log}}…",
|
||||||
|
"installation_complete": "Telepítés befejezve",
|
||||||
|
"installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve"
|
||||||
|
},
|
||||||
|
"catalogue": {
|
||||||
|
"search": "Szűrés…",
|
||||||
|
"developers": "Fejlesztők",
|
||||||
|
"genres": "Műfajok",
|
||||||
|
"tags": "Címkék",
|
||||||
|
"publishers": "Kiadók",
|
||||||
|
"download_sources": "Letöltési források",
|
||||||
|
"result_count": "{{resultCount}} találatok",
|
||||||
|
"filter_count": "{{filterCount}} elérhető",
|
||||||
|
"clear_filters": "{{filterCount}} kiválaszott szűrő törlése"
|
||||||
},
|
},
|
||||||
"catalogue": {},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Letöltési lehetőségek",
|
"open_download_options": "Letöltési opciók megnyitása",
|
||||||
"download_options_zero": "Nincs letöltési lehetőség",
|
"download_options_zero": "Nincs letöltési opció",
|
||||||
"download_options_one": "{{count}} letöltési lehetőség",
|
"download_options_one": "{{count}} letöltési opció",
|
||||||
"download_options_other": "{{count}} letöltési lehetőség",
|
"download_options_other": "{{count}} letöltési opció",
|
||||||
"updated_at": "Frissítve: {{updated_at}}",
|
"updated_at": "Frissítve: {{updated_at}}",
|
||||||
"install": "Letöltés",
|
"install": "Letöltés",
|
||||||
"resume": "Folytatás",
|
"resume": "Folytatás",
|
||||||
@@ -43,11 +134,13 @@
|
|||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
||||||
"eta": "Befejezés {{eta}}",
|
"eta": "Befejezés {{eta}}",
|
||||||
"downloading_metadata": "Metaadatok letöltése…",
|
"calculating_eta": "Hátralevő idő kiszámítása…",
|
||||||
|
"downloading_metadata": "Metaadat letöltése",
|
||||||
"filter": "Repackek szűrése",
|
"filter": "Repackek szűrése",
|
||||||
"requirements": "Rendszerkövetelmények",
|
"requirements": "Rendszerkövetelmények",
|
||||||
"minimum": "Minimális",
|
"minimum": "Minimális",
|
||||||
"recommended": "Ajánlott",
|
"recommended": "Ajánlott",
|
||||||
|
"paused": "Szüneteltetve",
|
||||||
"release_date": "Megjelenés: {{date}}",
|
"release_date": "Megjelenés: {{date}}",
|
||||||
"publisher": "Kiadta: {{publisher}}",
|
"publisher": "Kiadta: {{publisher}}",
|
||||||
"hours": "óra",
|
"hours": "óra",
|
||||||
@@ -55,29 +148,223 @@
|
|||||||
"amount_hours": "{{amount}} óra",
|
"amount_hours": "{{amount}} óra",
|
||||||
"amount_minutes": "{{amount}} perc",
|
"amount_minutes": "{{amount}} perc",
|
||||||
"accuracy": "{{accuracy}}% pontosság",
|
"accuracy": "{{accuracy}}% pontosság",
|
||||||
"add_to_library": "Hozzáadás a könyvtárhoz",
|
"add_to_library": "Könyvtárba helyezés",
|
||||||
|
"already_in_library": "Már könyvtárban",
|
||||||
"remove_from_library": "Eltávolítás a könyvtárból",
|
"remove_from_library": "Eltávolítás a könyvtárból",
|
||||||
"no_downloads": "Nincs elérhető letöltés",
|
"no_downloads": "Nincs elérhető letöltés",
|
||||||
"play_time": "Játszva: {{amount}}",
|
"play_time": "Játszva: {{amount}}",
|
||||||
"last_time_played": "Utoljára játszva {{period}}",
|
"last_time_played": "Utoljára játszva: {{period}}",
|
||||||
"not_played_yet": "{{title}} még nem játszottál",
|
"not_played_yet": "Ezzel a játékkal még nem játszottál: {{title}}",
|
||||||
"next_suggestion": "Következő javaslat",
|
"next_suggestion": "Következő javaslat",
|
||||||
"play": "Játék",
|
"play": "Játék",
|
||||||
"deleting": "Telepítő törlése…",
|
"deleting": "Telepítő törlése…",
|
||||||
"close": "Bezárás",
|
"close": "Bezárás",
|
||||||
"playing_now": "Jelenleg játszva",
|
"playing_now": "Játékban: ",
|
||||||
"change": "Változtatás",
|
"change": "Változtatás",
|
||||||
"repacks_modal_description": "Choose the repack you want to download",
|
"repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni",
|
||||||
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
|
"select_folder_hint": "A letöltési mappát a <0>Beállítások</0> menüjében változtathatod meg",
|
||||||
"download_now": "Töltsd le most"
|
"download_now": "Letöltés",
|
||||||
|
"no_shop_details": "A bolt adatai nem érhetőek el.",
|
||||||
|
"download_options": "Letöltési opciók",
|
||||||
|
"download_path": "Letöltis hely",
|
||||||
|
"previous_screenshot": "Előző screenshot",
|
||||||
|
"next_screenshot": "Következő screenshot",
|
||||||
|
"screenshot": "Screenshot {{number}}",
|
||||||
|
"open_screenshot": "Screenshot megnyitása {{number}}",
|
||||||
|
"download_settings": "Letöltési beállítások",
|
||||||
|
"downloader": "Letöltési mód",
|
||||||
|
"select_executable": "Tallózás",
|
||||||
|
"no_executable_selected": "Nincs futtatható fájl tallózva",
|
||||||
|
"open_folder": "Mappa megnyitása",
|
||||||
|
"open_download_location": "Letöltött fájlok megtekintése",
|
||||||
|
"create_shortcut": "Asztali parancsikon létrehozása",
|
||||||
|
"create_shortcut_simple": "Parancsikon létrehozása",
|
||||||
|
"clear": "Visszavon",
|
||||||
|
"remove_files": "Fájlok eltávolítása",
|
||||||
|
"remove_from_library_title": "Biztos vagy ebben?",
|
||||||
|
"remove_from_library_description": "Ezzel eltávolítod a játékot {{game}} a könyvtáradból",
|
||||||
|
"options": "Beállítások",
|
||||||
|
"properties": "További beállítások",
|
||||||
|
"executable_section_title": "Futtatható fájl",
|
||||||
|
"executable_section_description": "A fájl amely futtatásra fog kerülni amikor a \"Játék\" lenyomásra kerül",
|
||||||
|
"downloads_section_title": "Letöltések",
|
||||||
|
"downloads_section_description": "Csekkold le a játék frissítéseit vagy más verzióit",
|
||||||
|
"danger_zone_section_title": "Veszélyzóna",
|
||||||
|
"danger_zone_section_description": "Itt eltávolítható a játék a könyvtáradból, vagy a fájlok amelyek a Hydra által lettek letöltve",
|
||||||
|
"download_in_progress": "Letöltés folyamatban",
|
||||||
|
"download_paused": "Letöltés szüneteltetve",
|
||||||
|
"last_downloaded_option": "Utoljára letöltött",
|
||||||
|
"create_steam_shortcut": "Steam parancsikon létrehozása",
|
||||||
|
"create_shortcut_success": "A parancsikon létrehozása sikeres",
|
||||||
|
"you_might_need_to_restart_steam": "Lehetséges hogy újrakell indítsd a Steamet hogy lásd a változást.",
|
||||||
|
"create_shortcut_error": "Hiba lépett fel létrehozás közben",
|
||||||
|
"add_to_favorites": "Kedvencekhez adás",
|
||||||
|
"remove_from_favorites": "Eltávolítás a kedvencek közül",
|
||||||
|
"failed_update_favorites": "Kedvencek frissítése sikertelen",
|
||||||
|
"game_removed_from_library": "Játék eltávolítva a könyvtárból",
|
||||||
|
"failed_remove_from_library": "Játék eltávolítása a könyvtárból sikertelen",
|
||||||
|
"files_removed_success": "Fájlok eltávolítása sikeres",
|
||||||
|
"failed_remove_files": "Fájlok eltávolítása sikertelen",
|
||||||
|
"nsfw_content_title": "Ez a játék tartalmaz nem megfelelő tartalmat",
|
||||||
|
"nsfw_content_description": "A(z) {{title}} tartalma lehetséges hogy nem megfelelő minden korosztály számára. Biztosan folytatni szeretnéd?",
|
||||||
|
"allow_nsfw_content": "Folytatás",
|
||||||
|
"refuse_nsfw_content": "Vissza",
|
||||||
|
"stats": "Statisztikák",
|
||||||
|
"download_count": "Letöltések",
|
||||||
|
"player_count": "Aktív játékosok",
|
||||||
|
"rating_count": "Értékelés",
|
||||||
|
"download_error": "Ez a letöltési opció nem elérhető",
|
||||||
|
"download": "Letöltés",
|
||||||
|
"executable_path_in_use": "Ez a futtatható fájl már használatban van a(z) \"{{game}}\" által",
|
||||||
|
"warning": "Figyelmeztetés:",
|
||||||
|
"hydra_needs_to_remain_open": "ehhez a letöltéshez, a Hydrának muszáj nyitva maradnia hogy letöltődjön. Ha a Hydra bezáródik letöltés előtt, a letöltés elveszik.",
|
||||||
|
"achievements": "Achievementek",
|
||||||
|
"achievements_count": "Achievementek {{unlockedCount}}/{{achievementsCount}}",
|
||||||
|
"show_more": "Mutass többet",
|
||||||
|
"show_less": "Mutass kevesebbet",
|
||||||
|
"reviews": "Vélemények",
|
||||||
|
"leave_a_review": "Hagyd itt a véleményed",
|
||||||
|
"write_review_placeholder": "Oszd meg a gondolataid a játékról...",
|
||||||
|
"sort_newest": "Legújabb",
|
||||||
|
"no_reviews_yet": "Még nem lett vélemény megosztva",
|
||||||
|
"be_first_to_review": "Légy az első, aki megossza a véleményét a játékról!",
|
||||||
|
"sort_oldest": "Legrégibb",
|
||||||
|
"sort_highest_score": "Legmagasabb Pontszám",
|
||||||
|
"sort_lowest_score": "Legalacsonyabb Pontszám",
|
||||||
|
"sort_most_voted": "Legszavazottabb",
|
||||||
|
"rating": "Értékelés",
|
||||||
|
"rating_stats": "Értékelés",
|
||||||
|
"rating_very_negative": "Nagyon Negatív",
|
||||||
|
"rating_negative": "Negatív",
|
||||||
|
"rating_neutral": "Átlagos",
|
||||||
|
"rating_positive": "Pozitív",
|
||||||
|
"rating_very_positive": "Nagyon Pozitív",
|
||||||
|
"submit_review": "Küldés",
|
||||||
|
"submitting": "Küldés alatt...",
|
||||||
|
"review_submitted_successfully": "Vélemény beküldve sikeresen!",
|
||||||
|
"review_submission_failed": "Vélemény beküldése sikertelen. Kérlek próbáld újra.",
|
||||||
|
"review_cannot_be_empty": "A vélemény mező nem lehet üres.",
|
||||||
|
"review_deleted_successfully": "Vélemény sikeresen törölve.",
|
||||||
|
"review_deletion_failed": "Vélemény törlése sikertelen. Kérlek próbáld újra.",
|
||||||
|
"loading_reviews": "Vélemények betöltése...",
|
||||||
|
"loading_more_reviews": "Több vélemény betöltése...",
|
||||||
|
"load_more_reviews": "Több vélemény betöltése",
|
||||||
|
"you_seemed_to_enjoy_this_game": "Úgy látszik élvezted ezt a játékot",
|
||||||
|
"would_you_recommend_this_game": "Szeretnél véleményt írni erről a játékról?",
|
||||||
|
"yes": "Igen",
|
||||||
|
"maybe_later": "Talán Később",
|
||||||
|
"cloud_save": "Mentés felhőben",
|
||||||
|
"cloud_save_description": "Mentsd el az előrehaladásod a felhőben, majd folytasd egy másik eszközön",
|
||||||
|
"backups": "Biztonsági másolatok",
|
||||||
|
"install_backup": "Telepít",
|
||||||
|
"delete_backup": "Töröl",
|
||||||
|
"create_backup": "Biztonsági másolat létrehozása",
|
||||||
|
"last_backup_date": "Utolsó biztonsági mentés {{date}}",
|
||||||
|
"no_backup_preview": "Ehhez a címhez nem található mentett játék",
|
||||||
|
"restoring_backup": "Biztonsági mentés helyreállítás: ({{progress}} kész)…",
|
||||||
|
"uploading_backup": "Biztonsági mentés feltöltése…",
|
||||||
|
"no_backups": "Még nem hoztál létre biztonsági másolatot ehhez a játékhoz",
|
||||||
|
"backup_uploaded": "Biztonsági mentés feltöltve",
|
||||||
|
"backup_failed": "Biztonsági mentés sikertelen",
|
||||||
|
"backup_deleted": "Biztonsági mentés törölve",
|
||||||
|
"backup_restored": "Biztonsági mentés helyreállítva",
|
||||||
|
"see_all_achievements": "Achievementlista megtekintése",
|
||||||
|
"sign_in_to_see_achievements": "Jelentkezz be hogy lásd az achievementjeid",
|
||||||
|
"mapping_method_automatic": "Automatikus",
|
||||||
|
"mapping_method_manual": "Kézi",
|
||||||
|
"mapping_method_label": "Térképezési módszer",
|
||||||
|
"files_automatically_mapped": "Fájlok automatikusan térképezve",
|
||||||
|
"no_backups_created": "Ehhez a játékhoz nincs biztonsági másolat létrehozva",
|
||||||
|
"manage_files": "Fájlok kezelése",
|
||||||
|
"loading_save_preview": "Mentett játék keresése…",
|
||||||
|
"wine_prefix": "Wine Prefix",
|
||||||
|
"wine_prefix_description": "A Wine környezet, amiben a játék fut",
|
||||||
|
"launch_options": "Indítási opciók",
|
||||||
|
"launch_options_description": "Indítási opciók testreszabása haladó felhasználóknak (kísérleti funkció)",
|
||||||
|
"launch_options_placeholder": "Nincs paraméter megadva",
|
||||||
|
"no_download_option_info": "Nincs elérhető információ",
|
||||||
|
"backup_deletion_failed": "Biztonsági mentés törlése sikertelen",
|
||||||
|
"max_number_of_artifacts_reached": "A játék biztonsági mentéseinek száma elérte a határt",
|
||||||
|
"achievements_not_sync": "Tekintsd meg hogyan kell szinkronizálni az achievementjeid",
|
||||||
|
"manage_files_description": "Kezeld mely fájlokról készül biztonsági másolat, és melyek állíthatók vissza",
|
||||||
|
"select_folder": "Mappa tallózása",
|
||||||
|
"backup_from": "Biztonsági másolat: {{date}}",
|
||||||
|
"automatic_backup_from": "Automatikus másolat: {{date}}",
|
||||||
|
"enable_automatic_cloud_sync": "Automatikus felhőalapú szinkronizálás engedélyezése",
|
||||||
|
"custom_backup_location_set": "Egyéni biztonsági mentési hely",
|
||||||
|
"no_directory_selected": "Nincs mappa tallózva",
|
||||||
|
"no_write_permission": "Nem lehet a mappába letölteni. Kattints ide további információért.",
|
||||||
|
"reset_achievements": "Achievementek nullázása",
|
||||||
|
"reset_achievements_description": "Ez az összes achievementet nullázza a {{game}} játékhoz",
|
||||||
|
"reset_achievements_title": "Biztos vagy ebben?",
|
||||||
|
"reset_achievements_success": "Achievementek sikeresen nullázva",
|
||||||
|
"reset_achievements_error": "Achievementek nullázása sikertelen",
|
||||||
|
"download_error_gofile_quota_exceeded": "Túllépted a Gofile havi kvótáját. Kérlek, várd meg amíg a kvóta lejár.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "A Real-Debrid fiókod nem jogosult új letöltésekre. Kérlek, ellenőrízd a fiókbeállításaidat, majd próbáld újra.",
|
||||||
|
"download_error_not_cached_on_real_debrid": "Ez a letöltés nem elérhető a Real-Debriden, és lekérdezni letöltési állapotot még nem lehet.",
|
||||||
|
"update_playtime_title": "Játékidő frissítése",
|
||||||
|
"update_playtime_description": "A(z) {{game}} játékidejének frissítése manuálisan",
|
||||||
|
"update_playtime": "Játékidő frissítése",
|
||||||
|
"update_playtime_success": "Játékidő sikeresen frissítve",
|
||||||
|
"update_playtime_error": "A Játékidőnek nem sikerült frissülnie",
|
||||||
|
"update_game_playtime": "Játékidő frissítése",
|
||||||
|
"manual_playtime_warning": "A Játékidő „Manuálisan frissített”-ként lesz megjelölve, és ez nem visszavonható.",
|
||||||
|
"manual_playtime_tooltip": "Ez a Játékidő manuálisan lett frissítve",
|
||||||
|
"download_error_not_cached_on_torbox": "Ez a letöltés nem elérhető a TorBoxon, és lekérdezni letöltési állapotot még nem lehet.",
|
||||||
|
"download_error_not_cached_on_hydra": "Ez a letöltés nem elérhető a Nimbuson.",
|
||||||
|
"game_removed_from_favorites": "Játék eltávolítva a kedvencek közül",
|
||||||
|
"game_added_to_favorites": "Játék hozzáadva a kedvencekhez",
|
||||||
|
"game_removed_from_pinned": "Játék eltávolítva a kitűzöttek közül",
|
||||||
|
"game_added_to_pinned": "Játék sikeresen kitűzve",
|
||||||
|
"automatically_extract_downloaded_files": "Automatikus kibontása a letöltött fájloknak",
|
||||||
|
"create_start_menu_shortcut": "Start menü parancsikon létrehozása",
|
||||||
|
"invalid_wine_prefix_path": "Érvénytelen Wine prefix elérési útvonal",
|
||||||
|
"invalid_wine_prefix_path_description": "Az út a Wine prefixhez érvénytelen. Ellenőrízd az elérési utat, majd próbáld újra.",
|
||||||
|
"missing_wine_prefix": "Wine prefix szükséges a biztonsági másolat létrehozásához Linux rendszeren.",
|
||||||
|
"artifact_renamed": "Biztonsági mentés sikeresen átnevezve",
|
||||||
|
"rename_artifact": "Biztonsági mentés átnevezése",
|
||||||
|
"rename_artifact_description": "Nevezd át a biztonsági másolatot egy leíróbb névre",
|
||||||
|
"artifact_name_label": "Biztonsági másolat neve",
|
||||||
|
"artifact_name_placeholder": "Adj egy nevet a biztonsági mentésnek",
|
||||||
|
"save_changes": "Változtatások mentése",
|
||||||
|
"required_field": "Ez a mező kötelező",
|
||||||
|
"max_length_field": "Ez a mező kevesebb karakter kell legyen mint {{length}}",
|
||||||
|
"freeze_backup": "Rögzítsd, hogy az automatikus biztonsági mentések ne írják felül",
|
||||||
|
"unfreeze_backup": "Leválaszt",
|
||||||
|
"backup_frozen": "Biztonsági mentés rögzítve",
|
||||||
|
"backup_unfrozen": "Biztonsági mentés leválasztva",
|
||||||
|
"backup_freeze_failed": "Biztonsági mentés rögzítése sikertelen",
|
||||||
|
"backup_freeze_failed_description": "Legalább egy szabad helyet kell hagyni az automatikus biztonsági mentéseknek",
|
||||||
|
"edit_game_modal_button": "Játékadatok testreszabása",
|
||||||
|
"game_details": "Játék leírása",
|
||||||
|
"currency_symbol": "Ft",
|
||||||
|
"currency_country": "hu",
|
||||||
|
"prices": "Árak",
|
||||||
|
"no_prices_found": "Nincsenek található árak",
|
||||||
|
"view_all_prices": "Összes ár megtekintése",
|
||||||
|
"retail_price": "Bolti ár",
|
||||||
|
"keyshop_price": "Nem hivatalos ár",
|
||||||
|
"historical_retail": "Korábbi bolti ár",
|
||||||
|
"historical_keyshop": "Korábbi nem hivatalos ár",
|
||||||
|
"language": "Nyelv",
|
||||||
|
"caption": "Felirat",
|
||||||
|
"audio": "Hang",
|
||||||
|
"filter_by_source": "Szűrés forrás szerint",
|
||||||
|
"no_repacks_found": "Nem található forrás ehhez a játékhoz",
|
||||||
|
"delete_review": "Vélemény törlése",
|
||||||
|
"remove_review": "Vélemény eltávolítása",
|
||||||
|
"delete_review_modal_title": "Biztos vagy abban hogy törölni szeretnéd a véleményed?",
|
||||||
|
"delete_review_modal_description": "Ez a lépés nem vonható vissza.",
|
||||||
|
"delete_review_modal_delete_button": "Törlés",
|
||||||
|
"delete_review_modal_cancel_button": "Mégse"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Hydra Aktiválása",
|
"title": "Hydra Aktiválása",
|
||||||
"installation_id": "Telepítési ID:",
|
"installation_id": "Telepítési Azonosító:",
|
||||||
"enter_activation_code": "Add meg az aktiválási kódodat",
|
"enter_activation_code": "Írd be az aktiválási kódod",
|
||||||
"message": "Ha nem tudod, hol kérdezd meg ezt, akkor nem is kellene, hogy legyen ilyened.",
|
"message": "Ha nem tudod hol kérdezz efelől, akkor nem kéne ilyened legyen.",
|
||||||
"activate": "Aktiválás",
|
"activate": "Aktiválás",
|
||||||
"loading": "Betöltés…"
|
"loading": "Töltés…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "Folytatás",
|
"resume": "Folytatás",
|
||||||
@@ -86,46 +373,343 @@
|
|||||||
"paused": "Szüneteltetve",
|
"paused": "Szüneteltetve",
|
||||||
"verifying": "Ellenőrzés…",
|
"verifying": "Ellenőrzés…",
|
||||||
"completed": "Befejezve",
|
"completed": "Befejezve",
|
||||||
|
"removed": "Nincs letöltve",
|
||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"filter": "Letöltött játékok szűrése",
|
"filter": "Letöltött játékok szűrése",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolít",
|
||||||
"downloading_metadata": "Metaadatok letöltése…",
|
"downloading_metadata": "Metaadatok letöltése…",
|
||||||
"deleting": "Telepítő törlése…",
|
"deleting": "Telepítő törlése…",
|
||||||
"delete": "Telepítő eltávolítása",
|
"delete": "Telepítő eltávolítása",
|
||||||
"delete_modal_title": "Biztos vagy benne?",
|
"delete_modal_title": "Biztos vagy ebben?",
|
||||||
"delete_modal_description": "Ez eltávolít minden telepítési fájlt a számítógépedről",
|
"delete_modal_description": "Ez eltávolítja a telepítési fájlokat a számítógépedről",
|
||||||
"install": "Telepítés"
|
"install": "Telepít",
|
||||||
|
"download_in_progress": "Folyamatban lévő",
|
||||||
|
"queued_downloads": "Várakozósoron lévő letöltések",
|
||||||
|
"downloads_completed": "Befejezett",
|
||||||
|
"queued": "Várakozási sorban",
|
||||||
|
"no_downloads_title": "Oly üres..",
|
||||||
|
"no_downloads_description": "Még nem töltöttél le semmit a Hydra segítségével, de soha nem késő elkezdeni.",
|
||||||
|
"checking_files": "Fájlok ellenőrzése…",
|
||||||
|
"seeding": "Seedelés",
|
||||||
|
"stop_seeding": "Seedelés leállítása",
|
||||||
|
"resume_seeding": "Seedelés folytatása",
|
||||||
|
"options": "Kezelés",
|
||||||
|
"alldebrid_size_not_supported": "Letöltési információ az AllDebrid-hez még nem támogatott",
|
||||||
|
"extract": "Fájlok kibontása",
|
||||||
|
"extracting": "Fájlok kibontása…"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Letöltések helye",
|
"downloads_path": "Letöltési útvonalak",
|
||||||
"change": "Frissítés",
|
"change": "Frissítés",
|
||||||
"notifications": "Értesítések",
|
"notifications": "Értesítések",
|
||||||
"enable_download_notifications": "Amikor egy letöltés befejeződik",
|
"enable_download_notifications": "Amikor egy letöltés befejeződik",
|
||||||
|
"enable_repack_list_notifications": "Amikor új repack kerül feltöltésre",
|
||||||
|
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||||
|
"quit_app_instead_hiding": "Hydra elrejtésének tiltása bezáráskor",
|
||||||
|
"launch_with_system": "Hydra automatikus indítása rendszer indításakor",
|
||||||
|
"general": "Általános",
|
||||||
|
"behavior": "Működés",
|
||||||
|
"download_sources": "Letöltési források",
|
||||||
|
"language": "Nyelv",
|
||||||
|
"api_token": "API Token",
|
||||||
|
"enable_real_debrid": "Real-Debrid Bekapcsolása",
|
||||||
|
"real_debrid_description": "A Real-Debrid egy korlátozásmentes letöltőprogram, ami lehetővé teszi a fájlok gyors letöltését, és csak az internetkapcsolat sebessége szab határt.",
|
||||||
|
"debrid_invalid_token": "Érvénytelen API token",
|
||||||
|
"debrid_api_token_hint": "Az API tokened <0>itt</0> található",
|
||||||
|
"real_debrid_free_account_error": "Ez a fiók: \"{{username}}\" egy ingyenes fiók. Kérlek iratkozz fel a Real-Debridre",
|
||||||
|
"debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ",
|
||||||
|
"save_changes": "Változtatások mentése",
|
||||||
|
"changes_saved": "Változtatások sikeresen mentve",
|
||||||
|
"download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
|
||||||
|
"validate_download_source": "Érvényesítés",
|
||||||
|
"remove_download_source": "Eltávolítás",
|
||||||
|
"add_download_source": "Forrás hozáadása",
|
||||||
|
"download_count_zero": "Nincs letöltési opció",
|
||||||
|
"download_count_one": "{{countFormatted}} letöltési opció",
|
||||||
|
"download_count_other": "{{countFormatted}} letöltési opció",
|
||||||
|
"download_source_url": "URL forrás:",
|
||||||
|
"add_download_source_description": "Helyezd be a .json fájl URL-jét",
|
||||||
|
"download_source_up_to_date": "Naprakész",
|
||||||
|
"download_source_errored": "Hiba történt",
|
||||||
|
"sync_download_sources": "Források szinkronizálása",
|
||||||
|
"removed_download_source": "Letöltési forrás eltávolítva",
|
||||||
|
"removed_download_sources": "Letöltési források eltávolítva",
|
||||||
"cancel_button_confirmation_delete_all_sources": "Nem",
|
"cancel_button_confirmation_delete_all_sources": "Nem",
|
||||||
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
|
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
|
||||||
"description_confirmation_delete_all_sources": "Törölni fog minden letöltési forrást",
|
"title_confirmation_delete_all_sources": "Az összes letöltési forrás törlése",
|
||||||
"title_confirmation_delete_all_sources": "Törölje az összes letöltési forrást",
|
"description_confirmation_delete_all_sources": "Az összes letöltési forrást törölni fogod ezáltal",
|
||||||
"removed_download_sources": "Betűtípusok eltávolítva",
|
"button_delete_all_sources": "Összes eltávolítása",
|
||||||
"button_delete_all_sources": "Távolítsa el az összes letöltési forrást",
|
"added_download_source": "Letöltési forrás hozzáadva",
|
||||||
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül"
|
"download_sources_synced": "Az összes letöltési forrás szinkronizálva",
|
||||||
|
"insert_valid_json_url": "Adj meg egy érvényes JSON url-t",
|
||||||
|
"found_download_option_zero": "Nincs letöltési opció",
|
||||||
|
"found_download_option_one": "{{countFormatted}} Letöltési opció találva",
|
||||||
|
"found_download_option_other": "{{countFormatted}} Letöltési opciók találva",
|
||||||
|
"import": "Importálás",
|
||||||
|
"public": "Publikus",
|
||||||
|
"private": "Privát",
|
||||||
|
"friends_only": "Csak barátok",
|
||||||
|
"privacy": "Adatvédelem",
|
||||||
|
"profile_visibility": "Profil láthatósága",
|
||||||
|
"profile_visibility_description": "Válaszd ki, ki láthatja a profilod és könyvtárad",
|
||||||
|
"required_field": "Ez a mező kötelező",
|
||||||
|
"source_already_exists": "Ez a forrás már használatban",
|
||||||
|
"must_be_valid_url": "A forrás egy érvényes URL kell legyen",
|
||||||
|
"blocked_users": "Letiltott felhasználók",
|
||||||
|
"user_unblocked": "Felhasználó letiltva",
|
||||||
|
"enable_achievement_notifications": "Amikor egy achievement feloldódik",
|
||||||
|
"launch_minimized": "Hydra indítása minimalizálva",
|
||||||
|
"disable_nsfw_alert": "NSFW figyelmeztetés kikapcsolása",
|
||||||
|
"seed_after_download_complete": "Letöltés utáni seedelés",
|
||||||
|
"show_hidden_achievement_description": "Rejtett achievementek leírásának megjelenítése feloldás előtt",
|
||||||
|
"account": "Fiók",
|
||||||
|
"no_users_blocked": "Nincsenek letiltott felhasználóid",
|
||||||
|
"subscription_active_until": "Hydra Cloud előfizetésed aktív, eddig: {{date}}",
|
||||||
|
"manage_subscription": "Előfizetés kezelése",
|
||||||
|
"update_email": "Email változtatása",
|
||||||
|
"update_password": "Jelszó változtatása",
|
||||||
|
"current_email": "Jelenlegi email:",
|
||||||
|
"no_email_account": "Még nincs beállított emailed",
|
||||||
|
"account_data_updated_successfully": "Fiókadatok változtatása sikeres",
|
||||||
|
"renew_subscription": "Hydra Cloud Megújítása",
|
||||||
|
"subscription_expired_at": "Az előfizetésed lejárt, ekkor: {{date}}",
|
||||||
|
"no_subscription": "Élvezd a Hydrát a lehető legjobb módon",
|
||||||
|
"become_subscriber": "Légy Hydra Cloud tag",
|
||||||
|
"subscription_renew_cancelled": "Automatikus megújítás kikapcsolva",
|
||||||
|
"subscription_renews_on": "Az előfizetésed megújul, ekkor: {{date}}",
|
||||||
|
"bill_sent_until": "A következő számlát ezen napon küldjük",
|
||||||
|
"no_themes": "Úgy látszik nincs egyetlen témád sem még, de ne aggódj, kattints ide hogy elkészítsd a remekművedet.",
|
||||||
|
"editor_tab_code": "Code",
|
||||||
|
"editor_tab_info": "Info",
|
||||||
|
"editor_tab_save": "Mentés",
|
||||||
|
"web_store": "Webáruház",
|
||||||
|
"clear_themes": "Törlés",
|
||||||
|
"create_theme": "Létrehozás",
|
||||||
|
"create_theme_modal_title": "Egyéni téma létrehozása",
|
||||||
|
"create_theme_modal_description": "Hozz létre egy új témát, hogy testreszabhasd a Hydrát ahogy szeretnéd",
|
||||||
|
"theme_name": "Téma neve",
|
||||||
|
"insert_theme_name": "Adj a témádnak nevet",
|
||||||
|
"set_theme": "Téma beállítása",
|
||||||
|
"unset_theme": "Téma visszavonása",
|
||||||
|
"delete_theme": "Téma törlése",
|
||||||
|
"edit_theme": "Téma szerkesztése",
|
||||||
|
"delete_all_themes": "Összes téma törlése",
|
||||||
|
"delete_all_themes_description": "Ez törölni fogja az összes témádat",
|
||||||
|
"delete_theme_description": "Ez törölni fogja a(z) {{theme}} témát",
|
||||||
|
"cancel": "Mégsem",
|
||||||
|
"appearance": "Megjelenés",
|
||||||
|
"debrid": "Debrid",
|
||||||
|
"debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, csak az internet sebességed szab határt.",
|
||||||
|
"enable_torbox": "TorBox bekapcsolása",
|
||||||
|
"torbox_description": "A TorBox egy olyan premium seedbox szolgáltatás, amely még a piacon elérhető legjobb szerverekkel is felveszi a versenyt.",
|
||||||
|
"torbox_account_linked": "TorBox fiók összekapcsolva",
|
||||||
|
"create_real_debrid_account": "Kattints ide ha még nincs Real-Debrid fiókod",
|
||||||
|
"create_torbox_account": "Kattints ide ha még nincs TorBox fiókod",
|
||||||
|
"real_debrid_account_linked": "Real-Debrid fiók összekapcsolva",
|
||||||
|
"enable_all_debrid": "All-Debrid bekapcsolása",
|
||||||
|
"all_debrid_description": "Az All-Debrid egy korlátozásmentes letöltőprogram, ami lehetővé teszi a fájlok gyors letöltését különböző forrásokból.",
|
||||||
|
"all_debrid_free_account_error": "Ez a fiók: \"{{username}}\" egy ingyenes fiók. Kérlek iratkozz fel az All-Debridre",
|
||||||
|
"all_debrid_account_linked": "All-Debrid fiók összekapcsolva",
|
||||||
|
"alldebrid_missing_key": "Kérlek adj meg egy API key-t",
|
||||||
|
"alldebrid_invalid_key": "Érvénytelen API key",
|
||||||
|
"alldebrid_blocked": "Az API key-ed Földrajzilag vagy IP-alapján van blokkolva",
|
||||||
|
"alldebrid_banned": "Ez a fiók kitiltásra került",
|
||||||
|
"alldebrid_unknown_error": "Egy ismeretlen hiba történt",
|
||||||
|
"alldebrid_invalid_response": "Érvénytelen válasz az All-Debrid felől",
|
||||||
|
"alldebrid_network_error": "Hálózati hiba. Ellenőrízd az internetkapcsolatod",
|
||||||
|
"name_min_length": "A téma neve legalább 3 karakter hosszú legyen",
|
||||||
|
"import_theme": "Téma importálása",
|
||||||
|
"import_theme_description": "Ezt a témát fogod importálni a Témaáruház-ból: {{theme}}",
|
||||||
|
"error_importing_theme": "Hiba lépett fel a téma importálása közben",
|
||||||
|
"theme_imported": "Téma sikeresen importálva",
|
||||||
|
"enable_friend_request_notifications": "Amikor ismerősnek jelölnek",
|
||||||
|
"enable_auto_install": "Frissítések letöltése automatikusan",
|
||||||
|
"common_redist": "Alapvető Segédprogramok",
|
||||||
|
"common_redist_description": "Egyes játékok futtatásához alapvető segédprogram fájlok szükségesek. A problémák elkerülése képpen ajánlott telepíteni őket.",
|
||||||
|
"install_common_redist": "Telepítés",
|
||||||
|
"installing_common_redist": "Telepítés alatt…",
|
||||||
|
"show_download_speed_in_megabytes": "Letöltési sebesség megabájt/másodpercben lévő megjelenítése",
|
||||||
|
"extract_files_by_default": "Fájlok kicsomagolása letöltés után",
|
||||||
|
"enable_steam_achievements": "Steam-achievementek utáni keresés engedélyezése",
|
||||||
|
"achievement_custom_notification_position": "Achievement-értesítések egyéni elhelyezése",
|
||||||
|
"top-left": "Bal felső sarok",
|
||||||
|
"top-center": "Felső közép",
|
||||||
|
"top-right": "Jobb felső sarok",
|
||||||
|
"bottom-left": "Bal alsó sarok",
|
||||||
|
"bottom-center": "Alsó közép",
|
||||||
|
"bottom-right": "Jobb alsó sarok",
|
||||||
|
"enable_achievement_custom_notifications": "Egyéni achievement-értesítések bekapcsolása",
|
||||||
|
"alignment": "Igazítás",
|
||||||
|
"variation": "Variáció",
|
||||||
|
"default": "Alapértelmezett",
|
||||||
|
"rare": "Ritka",
|
||||||
|
"platinum": "Platina",
|
||||||
|
"hidden": "Rejtett",
|
||||||
|
"test_notification": "Értesítés tesztelése",
|
||||||
|
"notification_preview": "Achievement Értesítés Előnézete",
|
||||||
|
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Letöltés befejeződött",
|
"download_complete": "Letöltés befejezve",
|
||||||
"game_ready_to_install": "{{title}} telepítésre kész",
|
"game_ready_to_install": "A(z) {{title}} telepítésre kész",
|
||||||
"repack_list_updated": "Repack lista frissítve",
|
"repack_list_updated": "Repack lista frissítve",
|
||||||
"repack_count_one": "{{count}} repack hozzáadva",
|
"repack_count_one": "{{count}} repack hozzáadva",
|
||||||
"repack_count_other": "{{count}} repack hozzáadva"
|
"repack_count_other": "{{count}} repack hozzáadva",
|
||||||
|
"new_update_available": "A(z) {{version}} verzió elérhető",
|
||||||
|
"restart_to_install_update": "Indítsd újra a Hydrát a frissítés telepítéséhez",
|
||||||
|
"notification_achievement_unlocked_title": "Achievement feloldva: {{game}}",
|
||||||
|
"notification_achievement_unlocked_body": "{{achievement}} és további {{count}} feloldva",
|
||||||
|
"new_friend_request_description": "{{displayName}} küldött neked egy barátfelkérést",
|
||||||
|
"new_friend_request_title": "Új barátfelkérés",
|
||||||
|
"extraction_complete": "Kicsomagolás befejezve",
|
||||||
|
"game_extracted": "{{title}} sikeresen kicsomagolva",
|
||||||
|
"friend_started_playing_game": "{{displayName}} játszani kezdett",
|
||||||
|
"test_achievement_notification_title": "Ez egy teszt értesítés",
|
||||||
|
"test_achievement_notification_description": "Elég menő, mi?"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Hydra megnyitása",
|
"open": "Hydra megnyitása",
|
||||||
"quit": "Kilépés"
|
"quit": "Kilépés"
|
||||||
},
|
},
|
||||||
"game_card": {
|
"game_card": {
|
||||||
"no_downloads": "Nincs elérhető letöltés"
|
"available_one": "Elérhető",
|
||||||
|
"available_other": "Elérhető",
|
||||||
|
"no_downloads": "Nincs elérhető letöltés",
|
||||||
|
"calculating": "Feldolgozás"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "A programok nincsenek telepítve",
|
"title": "A programok nincsenek telepítve",
|
||||||
"description": "A Wine vagy a Lutris végrehajtható fájljai nem találhatók a rendszereden",
|
"description": "Wine vagy Lutris futtatható fájlok nem találhatók a rendszereden",
|
||||||
"instructions": "Ellenőrizd a megfelelő telepítési módot bármelyiküknek a Linux disztribúciódon, hogy a játék normálisan fusson"
|
"instructions": "Ellenőrízd hogy melyiket kell helyesen telepíteni a Linux disztribúciódra, hogy a játék megfelelően fusson"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"close": "Bezárás gomb"
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"toggle_password_visibility": "Jelszó láthatóságának állítása"
|
||||||
|
},
|
||||||
|
"user_profile": {
|
||||||
|
"amount_hours": "{{amount}} óra",
|
||||||
|
"amount_minutes": "{{amount}} perc",
|
||||||
|
"amount_hours_short": "{{amount}}ó",
|
||||||
|
"amount_minutes_short": "{{amount}}p",
|
||||||
|
"last_time_played": "Utoljára játszva {{period}}",
|
||||||
|
"activity": "Legutóbbi tevékenység",
|
||||||
|
"library": "Könyvtár",
|
||||||
|
"pinned": "Kitűzve",
|
||||||
|
"achievements_earned": "Elért achievementek",
|
||||||
|
"played_recently": "Nemrég játszva",
|
||||||
|
"playtime": "Játszottidő",
|
||||||
|
"total_play_time": "Teljes játszottidő",
|
||||||
|
"manual_playtime_tooltip": "Ez a játszottidő manuálisan lett frissítve",
|
||||||
|
"no_recent_activity_title": "Hmmm… itt semmi sincs",
|
||||||
|
"no_recent_activity_description": "Mostanában nem játszottál semmivel. Hát ideje ezt megváltoztatni!",
|
||||||
|
"display_name": "Profilnév",
|
||||||
|
"saving": "Mentésben",
|
||||||
|
"save": "Mentés",
|
||||||
|
"edit_profile": "Profil Szerkesztése",
|
||||||
|
"saved_successfully": "Sikeresen elmentve",
|
||||||
|
"try_again": "Kérlek, próbálkozz újra",
|
||||||
|
"sign_out_modal_title": "Biztos vagy ebben?",
|
||||||
|
"cancel": "Mégsem",
|
||||||
|
"successfully_signed_out": "Sikeresen kijelentkezve",
|
||||||
|
"sign_out": "Kijelentkezés",
|
||||||
|
"playing_for": "Játékban: {{amount}}",
|
||||||
|
"sign_out_modal_text": "A könyvtár a jelenlegi fiókodhoz van csatolva. Kijelentkezéskor a könyvtár többé nem lesz látható, és az eddigi előrehaladás nem lesz mentve. Folytatod a kijelentkezést?",
|
||||||
|
"add_friends": "Barát bejelölés",
|
||||||
|
"add": "Elküld",
|
||||||
|
"friend_code": "Barát kód",
|
||||||
|
"see_profile": "Profil megtekintése",
|
||||||
|
"sending": "Küldés..",
|
||||||
|
"friend_request_sent": "Barátfelkérés elküldve",
|
||||||
|
"friends": "Barátok",
|
||||||
|
"friends_list": "Barát lista",
|
||||||
|
"user_not_found": "Felhasználó nem találva",
|
||||||
|
"block_user": "Felhasználó letiltása",
|
||||||
|
"add_friend": "Barát bejelölése",
|
||||||
|
"request_sent": "Kérés elküldve",
|
||||||
|
"request_received": "Barátfelkérést kaptál",
|
||||||
|
"accept_request": "Kérés elfogadása",
|
||||||
|
"ignore_request": "Kérés ignorálása",
|
||||||
|
"cancel_request": "Kérés visszavonása",
|
||||||
|
"undo_friendship": "Barát eltávolítása",
|
||||||
|
"request_accepted": "Barátfelkérés elfogadva",
|
||||||
|
"user_blocked_successfully": "Felhasználó sikeresen letiltva",
|
||||||
|
"user_block_modal_text": "Ez által letiltod őt: {{displayName}}",
|
||||||
|
"blocked_users": "Letiltott felhasználók",
|
||||||
|
"unblock": "Tiltás feloldása",
|
||||||
|
"no_friends_added": "Nincs bejelölt barátod",
|
||||||
|
"pending": "Függőben",
|
||||||
|
"no_pending_invites": "Nincs függőben lévő barátfelkérésed",
|
||||||
|
"no_blocked_users": "Nincs letiltott felhasználó",
|
||||||
|
"friend_code_copied": "Barát kód kimásolva",
|
||||||
|
"undo_friendship_modal_text": "Ezáltal megszünteted a barátságod vele: {{displayName}}",
|
||||||
|
"privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállítások</0> menüjébe",
|
||||||
|
"locked_profile": "Ez a profil privát",
|
||||||
|
"image_process_failure": "Hiba a kép feldolgozása közben",
|
||||||
|
"required_field": "Ez a mező kötelező",
|
||||||
|
"displayname_min_length": "A megjelenített névnek legalább 3 karakter hosszúnak kell lennie",
|
||||||
|
"displayname_max_length": "A megjelenített név hossza legfeljebb 50 karakter lehet",
|
||||||
|
"report_profile": "Profil bejelentése",
|
||||||
|
"report_reason": "Miért jelented ezt a profilt?",
|
||||||
|
"report_description": "További információ",
|
||||||
|
"report_description_placeholder": "További információ",
|
||||||
|
"report": "Bejelentés",
|
||||||
|
"report_reason_hate": "Gyűlöletbeszéd",
|
||||||
|
"report_reason_sexual_content": "Szexuális tartalom",
|
||||||
|
"report_reason_violence": "Fenyegető",
|
||||||
|
"report_reason_spam": "Spam",
|
||||||
|
"report_reason_other": "Egyéb",
|
||||||
|
"profile_reported": "Profil bejelentve",
|
||||||
|
"your_friend_code": "A barát kódod:",
|
||||||
|
"upload_banner": "Borítókép feltöltés",
|
||||||
|
"uploading_banner": "Borítókép feltöltése…",
|
||||||
|
"background_image_updated": "Borítókép frissítve",
|
||||||
|
"stats": "Statisztikák",
|
||||||
|
"achievements": "achievementek",
|
||||||
|
"games": "Játékok",
|
||||||
|
"top_percentile": "Top {{percentile}}%",
|
||||||
|
"ranking_updated_weekly": "A rangsor hetente frissül.",
|
||||||
|
"playing": "Játékban: {{game}}",
|
||||||
|
"achievements_unlocked": "Achievementek feloldva",
|
||||||
|
"earned_points": "Megszerzett pontok",
|
||||||
|
"show_achievements_on_profile": "Mutasd az achievementjeid a profilodon",
|
||||||
|
"show_points_on_profile": "Mutasd a megszerzett pontjaid a profilodon",
|
||||||
|
"error_adding_friend": "Hiba, barátfelkérés sikertelen. Kérlek ellenőrízd a barát kódot",
|
||||||
|
"friend_code_length_error": "A barát kódnak 8 karakterből kell állnia",
|
||||||
|
"game_removed_from_pinned": "Játék eltávolítva a kitűzöttek közül",
|
||||||
|
"game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez",
|
||||||
|
"karma": "Karma",
|
||||||
|
"karma_count": "karma",
|
||||||
|
"karma_description": "Pozitív értékelésekre kapott pontok alapján"
|
||||||
|
},
|
||||||
|
"achievement": {
|
||||||
|
"achievement_unlocked": "Achievement feloldva",
|
||||||
|
"user_achievements": "{{displayName}} achievementjei",
|
||||||
|
"your_achievements": "A te achievementjeid",
|
||||||
|
"unlocked_at": "Feloldva: {{date}}",
|
||||||
|
"subscription_needed": "A tartalom megtekintéséhez Hydra Cloud előfizetés szükséges",
|
||||||
|
"new_achievements_unlocked": "{{achievementCount}} új achievement feloldva {{gameCount}} játékban",
|
||||||
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementek",
|
||||||
|
"achievements_unlocked_for_game": "{{achievementCount}} új achievement feloldva itt: {{gameTitle}}",
|
||||||
|
"hidden_achievement_tooltip": "Ez egy rejtett achievement",
|
||||||
|
"achievement_earn_points": "Szerezz be {{points}} pontot ezzel az achievement-el",
|
||||||
|
"earned_points": "Megszerzett pontok:",
|
||||||
|
"available_points": "Elérhető pontok:",
|
||||||
|
"how_to_earn_achievements_points": "Hogy lehet elérni achievement pontokat?"
|
||||||
|
},
|
||||||
|
"hydra_cloud": {
|
||||||
|
"subscription_tour_title": "Hydra Cloud Előfizetés",
|
||||||
|
"subscribe_now": "Előfizetés",
|
||||||
|
"cloud_saving": "Felhőmentés",
|
||||||
|
"cloud_achievements": "Mentsd az achievementjeid el a felhőben",
|
||||||
|
"animated_profile_picture": "Animált profilkép",
|
||||||
|
"premium_support": "Premium Támogatás",
|
||||||
|
"show_and_compare_achievements": "Jelenítsd és hasonlítsd az elért achievementjeid másokéhoz",
|
||||||
|
"animated_profile_banner": "Animált profil borítókép",
|
||||||
|
"hydra_cloud": "Hydra Cloud",
|
||||||
|
"hydra_cloud_feature_found": "Épp felfedeztél egy Hydra Cloud funkciót!",
|
||||||
|
"learn_more": "Tudj meg többet",
|
||||||
|
"debrid_description": "Akár 4x gyorsabb letöltés a Nimbusszal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@
|
|||||||
"open_download_location": "Lihat file yang diunduh",
|
"open_download_location": "Lihat file yang diunduh",
|
||||||
"create_shortcut": "Buat pintasan desktop",
|
"create_shortcut": "Buat pintasan desktop",
|
||||||
"remove_files": "Hapus file",
|
"remove_files": "Hapus file",
|
||||||
"remove_from_library_title": "Apa kamu yakin?",
|
|
||||||
"remove_from_library_description": "Ini akan menghapus {{game}} dari perpustakaan kamu",
|
"remove_from_library_description": "Ini akan menghapus {{game}} dari perpustakaan kamu",
|
||||||
"options": "Opsi",
|
"options": "Opsi",
|
||||||
"executable_section_title": "Eksekusi",
|
"executable_section_title": "Eksekusi",
|
||||||
|
|||||||
@@ -27,21 +27,68 @@
|
|||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "Precisa de ajuda?",
|
"need_help": "Precisa de ajuda?",
|
||||||
"favorites": "Favoritos",
|
"favorites": "Favoritos",
|
||||||
|
"playable_button_title": "Mostrar apenas jogos que você pode jogar agora",
|
||||||
"add_custom_game_tooltip": "Adicionar jogo personalizado",
|
"add_custom_game_tooltip": "Adicionar jogo personalizado",
|
||||||
|
"show_playable_only_tooltip": "Mostrar Apenas Jogáveis",
|
||||||
"custom_game_modal": "Adicionar jogo personalizado",
|
"custom_game_modal": "Adicionar jogo personalizado",
|
||||||
|
"custom_game_modal_description": "Adicione um jogo personalizado à sua biblioteca selecionando um arquivo executável",
|
||||||
|
"custom_game_modal_executable_path": "Caminho do Executável",
|
||||||
|
"custom_game_modal_select_executable": "Selecionar arquivo executável",
|
||||||
|
"custom_game_modal_title": "Título",
|
||||||
|
"custom_game_modal_enter_title": "Insira o título",
|
||||||
"edit_game_modal_title": "Título",
|
"edit_game_modal_title": "Título",
|
||||||
"playable_button_title": "",
|
|
||||||
"custom_game_modal_add": "Adicionar Jogo",
|
|
||||||
"custom_game_modal_adding": "Adicionando...",
|
|
||||||
"custom_game_modal_browse": "Buscar",
|
"custom_game_modal_browse": "Buscar",
|
||||||
"custom_game_modal_cancel": "Cancelar",
|
"custom_game_modal_cancel": "Cancelar",
|
||||||
"edit_game_modal_assets": "Imagens",
|
"custom_game_modal_add": "Adicionar Jogo",
|
||||||
"edit_game_modal_icon": "Ícone",
|
"custom_game_modal_adding": "Adicionando...",
|
||||||
"edit_game_modal_browse": "Buscar",
|
"custom_game_modal_success": "Jogo personalizado adicionado com sucesso",
|
||||||
"edit_game_modal_cancel": "Cancelar",
|
"custom_game_modal_failed": "Falha ao adicionar jogo personalizado",
|
||||||
|
"custom_game_modal_executable": "Executável",
|
||||||
|
"edit_game_modal": "Personalizar detalhes",
|
||||||
|
"edit_game_modal_description": "Personalize os recursos e detalhes do jogo",
|
||||||
"edit_game_modal_enter_title": "Insira o título",
|
"edit_game_modal_enter_title": "Insira o título",
|
||||||
|
"edit_game_modal_image": "Imagem",
|
||||||
|
"edit_game_modal_select_image": "Selecionar imagem",
|
||||||
|
"edit_game_modal_browse": "Buscar",
|
||||||
|
"edit_game_modal_image_preview": "Visualização da imagem",
|
||||||
|
"edit_game_modal_icon": "Ícone",
|
||||||
|
"edit_game_modal_select_icon": "Selecionar ícone",
|
||||||
|
"edit_game_modal_icon_preview": "Visualização do ícone",
|
||||||
"edit_game_modal_logo": "Logo",
|
"edit_game_modal_logo": "Logo",
|
||||||
"edit_game_modal": "Personalizar detalhes"
|
"edit_game_modal_select_logo": "Selecionar logo",
|
||||||
|
"edit_game_modal_logo_preview": "Visualização do logo",
|
||||||
|
"edit_game_modal_hero": "Hero da Biblioteca",
|
||||||
|
"edit_game_modal_select_hero": "Selecionar imagem hero da biblioteca",
|
||||||
|
"edit_game_modal_hero_preview": "Visualização da imagem hero da biblioteca",
|
||||||
|
"edit_game_modal_cancel": "Cancelar",
|
||||||
|
"edit_game_modal_update": "Atualizar",
|
||||||
|
"edit_game_modal_updating": "Atualizando...",
|
||||||
|
"edit_game_modal_fill_required": "Por favor, preencha todos os campos obrigatórios",
|
||||||
|
"edit_game_modal_success": "Recursos atualizados com sucesso",
|
||||||
|
"edit_game_modal_failed": "Falha ao atualizar recursos",
|
||||||
|
"edit_game_modal_image_filter": "Imagem",
|
||||||
|
"edit_game_modal_icon_resolution": "Resolução recomendada: 256x256px",
|
||||||
|
"edit_game_modal_logo_resolution": "Resolução recomendada: 640x360px",
|
||||||
|
"edit_game_modal_hero_resolution": "Resolução recomendada: 1920x620px",
|
||||||
|
"edit_game_modal_assets": "Imagens",
|
||||||
|
"edit_game_modal_drop_icon_image_here": "Solte a imagem do ícone aqui",
|
||||||
|
"edit_game_modal_drop_logo_image_here": "Solte a imagem do logo aqui",
|
||||||
|
"edit_game_modal_drop_hero_image_here": "Solte a imagem hero aqui",
|
||||||
|
"edit_game_modal_drop_to_replace_icon": "Solte para substituir o ícone",
|
||||||
|
"edit_game_modal_drop_to_replace_logo": "Solte para substituir o logo",
|
||||||
|
"edit_game_modal_drop_to_replace_hero": "Solte para substituir o hero",
|
||||||
|
"install_decky_plugin": "Instalar Plugin Decky",
|
||||||
|
"update_decky_plugin": "Atualizar Plugin Decky",
|
||||||
|
"decky_plugin_installed_version": "Plugin Decky (v{{version}})",
|
||||||
|
"install_decky_plugin_title": "Instalar Plugin Hydra Decky",
|
||||||
|
"install_decky_plugin_message": "Isso irá baixar e instalar o plugin Hydra para Decky Loader. Pode ser necessário permissões elevadas. Continuar?",
|
||||||
|
"update_decky_plugin_title": "Atualizar Plugin Hydra Decky",
|
||||||
|
"update_decky_plugin_message": "Uma nova versão do plugin Hydra Decky está disponível. Gostaria de atualizar agora?",
|
||||||
|
"decky_plugin_installed": "Plugin Decky v{{version}} instalado com sucesso",
|
||||||
|
"decky_plugin_installation_failed": "Falha ao instalar plugin Decky: {{error}}",
|
||||||
|
"decky_plugin_installation_error": "Erro ao instalar plugin Decky: {{error}}",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
@@ -120,8 +167,10 @@
|
|||||||
"open_folder": "Abrir pasta",
|
"open_folder": "Abrir pasta",
|
||||||
"open_download_location": "Ver arquivos baixados",
|
"open_download_location": "Ver arquivos baixados",
|
||||||
"create_shortcut": "Criar atalho na área de trabalho",
|
"create_shortcut": "Criar atalho na área de trabalho",
|
||||||
|
"create_shortcut_simple": "Criar atalho",
|
||||||
"remove_files": "Remover arquivos",
|
"remove_files": "Remover arquivos",
|
||||||
"options": "Gerenciar",
|
"options": "Gerenciar",
|
||||||
|
"properties": "Propriedades",
|
||||||
"remove_from_library_description": "Isso irá remover {{game}} da sua biblioteca",
|
"remove_from_library_description": "Isso irá remover {{game}} da sua biblioteca",
|
||||||
"remove_from_library_title": "Tem certeza?",
|
"remove_from_library_title": "Tem certeza?",
|
||||||
"executable_section_title": "Executável",
|
"executable_section_title": "Executável",
|
||||||
@@ -163,6 +212,7 @@
|
|||||||
"uploading_backup": "Criando backup…",
|
"uploading_backup": "Criando backup…",
|
||||||
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
||||||
"backup_uploaded": "Backup criado",
|
"backup_uploaded": "Backup criado",
|
||||||
|
"backup_failed": "Falha no backup",
|
||||||
"backup_deleted": "Backup apagado",
|
"backup_deleted": "Backup apagado",
|
||||||
"backup_restored": "Backup restaurado",
|
"backup_restored": "Backup restaurado",
|
||||||
"see_all_achievements": "Ver todas as conquistas",
|
"see_all_achievements": "Ver todas as conquistas",
|
||||||
@@ -204,6 +254,13 @@
|
|||||||
"download_error_not_cached_on_hydra": "Este download não está disponível no Nimbus.",
|
"download_error_not_cached_on_hydra": "Este download não está disponível no Nimbus.",
|
||||||
"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",
|
||||||
|
"add_to_favorites": "Adicionar aos favoritos",
|
||||||
|
"remove_from_favorites": "Remover dos favoritos",
|
||||||
|
"failed_update_favorites": "Falha ao atualizar favoritos",
|
||||||
|
"game_removed_from_library": "Jogo removido da biblioteca",
|
||||||
|
"failed_remove_from_library": "Falha ao remover da biblioteca",
|
||||||
|
"files_removed_success": "Arquivos removidos com sucesso",
|
||||||
|
"failed_remove_files": "Falha ao remover arquivos",
|
||||||
"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": "Caminho do prefixo Wine inválido",
|
||||||
@@ -235,6 +292,8 @@
|
|||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"caption": "Legenda",
|
"caption": "Legenda",
|
||||||
"audio": "Áudio",
|
"audio": "Áudio",
|
||||||
|
"filter_by_source": "Filtrar por fonte",
|
||||||
|
"no_repacks_found": "Nenhuma fonte encontrada para este jogo",
|
||||||
"edit_game_modal_button": "Alterar detalhes do jogo",
|
"edit_game_modal_button": "Alterar detalhes do jogo",
|
||||||
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||||
"game_removed_from_pinned": "Jogo removido dos fixados",
|
"game_removed_from_pinned": "Jogo removido dos fixados",
|
||||||
@@ -245,7 +304,48 @@
|
|||||||
"update_playtime": "Modificar tempo de jogo",
|
"update_playtime": "Modificar tempo de jogo",
|
||||||
"update_playtime_description": "Atualizar manualmente o tempo de jogo de {{game}}",
|
"update_playtime_description": "Atualizar manualmente o tempo de jogo de {{game}}",
|
||||||
"update_playtime_error": "Falha ao atualizar tempo de jogo",
|
"update_playtime_error": "Falha ao atualizar tempo de jogo",
|
||||||
"update_playtime_title": "Atualizar tempo de jogo"
|
"update_playtime_title": "Atualizar tempo de jogo",
|
||||||
|
"update_playtime_success": "Tempo de jogo atualizado com sucesso",
|
||||||
|
"show_more": "Mostrar mais",
|
||||||
|
"show_less": "Mostrar menos",
|
||||||
|
"reviews": "Avaliações",
|
||||||
|
"leave_a_review": "Deixar uma Avaliação",
|
||||||
|
"write_review_placeholder": "Compartilhe seus pensamentos sobre este jogo...",
|
||||||
|
"sort_newest": "Mais Recentes",
|
||||||
|
"sort_oldest": "Mais Antigas",
|
||||||
|
"sort_highest_score": "Maior Nota",
|
||||||
|
"sort_lowest_score": "Menor Nota",
|
||||||
|
"sort_most_voted": "Mais Votadas",
|
||||||
|
"no_reviews_yet": "Ainda não há avaliações",
|
||||||
|
"be_first_to_review": "Seja o primeiro a compartilhar seus pensamentos sobre este jogo!",
|
||||||
|
"rating": "Avaliação",
|
||||||
|
"rating_stats": "Avaliação",
|
||||||
|
"rating_very_negative": "Muito Negativo",
|
||||||
|
"rating_negative": "Negativo",
|
||||||
|
"rating_neutral": "Neutro",
|
||||||
|
"rating_positive": "Positivo",
|
||||||
|
"rating_very_positive": "Muito Positivo",
|
||||||
|
"submit_review": "Enviar",
|
||||||
|
"submitting": "Enviando...",
|
||||||
|
"review_submitted_successfully": "Avaliação enviada com sucesso!",
|
||||||
|
"review_submission_failed": "Falha ao enviar avaliação. Por favor, tente novamente.",
|
||||||
|
"review_cannot_be_empty": "O campo de texto da avaliação não pode estar vazio.",
|
||||||
|
"review_deleted_successfully": "Avaliação excluída com sucesso.",
|
||||||
|
"review_deletion_failed": "Falha ao excluir avaliação. Por favor, tente novamente.",
|
||||||
|
"loading_reviews": "Carregando avaliações...",
|
||||||
|
"loading_more_reviews": "Carregando mais avaliações...",
|
||||||
|
"load_more_reviews": "Carregar Mais Avaliações",
|
||||||
|
"you_seemed_to_enjoy_this_game": "Parece que você gostou deste jogo",
|
||||||
|
"would_you_recommend_this_game": "Gostaria de deixar uma avaliação para este jogo?",
|
||||||
|
"yes": "Sim",
|
||||||
|
"maybe_later": "Talvez Mais Tarde",
|
||||||
|
"delete_review": "Excluir avaliação",
|
||||||
|
"remove_review": "Remover Avaliação",
|
||||||
|
"delete_review_modal_title": "Tem certeza de que deseja excluir sua avaliação?",
|
||||||
|
"delete_review_modal_description": "Esta ação não pode ser desfeita.",
|
||||||
|
"delete_review_modal_delete_button": "Excluir",
|
||||||
|
"delete_review_modal_cancel_button": "Cancelar",
|
||||||
|
"rating_count": "Avaliação"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@@ -283,6 +383,7 @@
|
|||||||
"stop_seeding": "Parar de semear",
|
"stop_seeding": "Parar de semear",
|
||||||
"resume_seeding": "Semear",
|
"resume_seeding": "Semear",
|
||||||
"options": "Gerenciar",
|
"options": "Gerenciar",
|
||||||
|
"alldebrid_size_not_supported": "Informações de download para AllDebrid ainda não são suportadas",
|
||||||
"extract": "Extrair arquivos",
|
"extract": "Extrair arquivos",
|
||||||
"extracting": "Extraindo arquivos…"
|
"extracting": "Extraindo arquivos…"
|
||||||
},
|
},
|
||||||
@@ -367,6 +468,8 @@
|
|||||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
||||||
"no_themes": "Parece que você ainda não tem nenhum tema. Não se preocupe, clique aqui para criar sua primeira obra de arte.",
|
"no_themes": "Parece que você ainda não tem nenhum tema. Não se preocupe, clique aqui para criar sua primeira obra de arte.",
|
||||||
|
"editor_tab_code": "Código",
|
||||||
|
"editor_tab_info": "Info",
|
||||||
"editor_tab_save": "Salvar",
|
"editor_tab_save": "Salvar",
|
||||||
"web_store": "Loja de temas",
|
"web_store": "Loja de temas",
|
||||||
"clear_themes": "Limpar",
|
"clear_themes": "Limpar",
|
||||||
@@ -384,12 +487,25 @@
|
|||||||
"delete_theme_description": "Isso irá deletar o tema {{theme}}",
|
"delete_theme_description": "Isso irá deletar o tema {{theme}}",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"appearance": "Aparência",
|
"appearance": "Aparência",
|
||||||
|
"debrid": "Debrid",
|
||||||
|
"debrid_description": "Serviços Debrid são downloaders premium sem restrições que permitem baixar rapidamente arquivos hospedados em vários serviços de hospedagem de arquivos, limitados apenas pela sua velocidade de internet.",
|
||||||
"enable_torbox": "Habilitar TorBox",
|
"enable_torbox": "Habilitar TorBox",
|
||||||
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||||
"torbox_account_linked": "Conta do TorBox vinculada",
|
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||||
"create_real_debrid_account": "Clique aqui se você ainda não tem uma conta do Real-Debrid",
|
"create_real_debrid_account": "Clique aqui se você ainda não tem uma conta do Real-Debrid",
|
||||||
"create_torbox_account": "Clique aqui se você ainda não tem uma conta do TorBox",
|
"create_torbox_account": "Clique aqui se você ainda não tem uma conta do TorBox",
|
||||||
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
||||||
|
"enable_all_debrid": "Habilitar All-Debrid",
|
||||||
|
"all_debrid_description": "All-Debrid é um downloader sem restrições que permite baixar rapidamente arquivos de várias fontes.",
|
||||||
|
"all_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine o All-Debrid",
|
||||||
|
"all_debrid_account_linked": "Conta All-Debrid vinculada com sucesso",
|
||||||
|
"alldebrid_missing_key": "Por favor, forneça uma chave de API",
|
||||||
|
"alldebrid_invalid_key": "Chave de API inválida",
|
||||||
|
"alldebrid_blocked": "Sua chave de API está bloqueada por geolocalização ou IP",
|
||||||
|
"alldebrid_banned": "Esta conta foi banida",
|
||||||
|
"alldebrid_unknown_error": "Ocorreu um erro desconhecido",
|
||||||
|
"alldebrid_invalid_response": "Resposta inválida do All-Debrid",
|
||||||
|
"alldebrid_network_error": "Erro de rede. Por favor, verifique sua conexão",
|
||||||
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
||||||
"import_theme": "Importar tema",
|
"import_theme": "Importar tema",
|
||||||
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
|
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
|
||||||
@@ -420,8 +536,7 @@
|
|||||||
"hidden": "Oculta",
|
"hidden": "Oculta",
|
||||||
"test_notification": "Testar notificação",
|
"test_notification": "Testar notificação",
|
||||||
"notification_preview": "Prévia da Notificação de Conquistas",
|
"notification_preview": "Prévia da Notificação de Conquistas",
|
||||||
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
|
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
|
||||||
"editor_tab_code": "Código"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
@@ -437,7 +552,9 @@
|
|||||||
"game_extracted": "{{title}} extraído com sucesso",
|
"game_extracted": "{{title}} extraído com sucesso",
|
||||||
"friend_started_playing_game": "{{displayName}} começou a jogar",
|
"friend_started_playing_game": "{{displayName}} começou a jogar",
|
||||||
"test_achievement_notification_title": "Esta é uma notificação de teste",
|
"test_achievement_notification_title": "Esta é uma notificação de teste",
|
||||||
"test_achievement_notification_description": "Bem legal, né?"
|
"test_achievement_notification_description": "Bem legal, né?",
|
||||||
|
"notification_achievement_unlocked_title": "Conquista desbloqueada para {{game}}",
|
||||||
|
"notification_achievement_unlocked_body": "{{achievement}} e outras {{count}} foram desbloqueadas"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Abrir Hydra",
|
"open": "Abrir Hydra",
|
||||||
@@ -446,7 +563,8 @@
|
|||||||
"game_card": {
|
"game_card": {
|
||||||
"available_one": "Disponível",
|
"available_one": "Disponível",
|
||||||
"available_other": "Disponíveis",
|
"available_other": "Disponíveis",
|
||||||
"no_downloads": "Sem downloads disponíveis"
|
"no_downloads": "Sem downloads disponíveis",
|
||||||
|
"calculating": "Calculando"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Programas não instalados",
|
"title": "Programas não instalados",
|
||||||
@@ -555,10 +673,15 @@
|
|||||||
"playtime": "Tempo de jogo",
|
"playtime": "Tempo de jogo",
|
||||||
"played_recently": "Jogado recentemente",
|
"played_recently": "Jogado recentemente",
|
||||||
"pinned": "Fixado",
|
"pinned": "Fixado",
|
||||||
"amount_minutes_short": "{{amount}}h",
|
"amount_minutes_short": "{{amount}}m",
|
||||||
"amount_hours_short": "{{amount}}h",
|
"amount_hours_short": "{{amount}}h",
|
||||||
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||||
"achievements_earned": "Conquistas recebidas"
|
"game_removed_from_pinned": "Jogo removido dos fixados",
|
||||||
|
"achievements_earned": "Conquistas recebidas",
|
||||||
|
"karma": "Karma",
|
||||||
|
"karma_count": "karma",
|
||||||
|
"karma_description": "Ganho a partir de curtidas positivas em avaliações",
|
||||||
|
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada",
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"uploading_backup": "A criar backup…",
|
"uploading_backup": "A criar backup…",
|
||||||
"no_backups": "Ainda não fizeste nenhum backup deste jogo",
|
"no_backups": "Ainda não fizeste nenhum backup deste jogo",
|
||||||
"backup_uploaded": "Backup criado",
|
"backup_uploaded": "Backup criado",
|
||||||
|
"backup_failed": "Falha ao criar backup",
|
||||||
"backup_deleted": "Backup apagado",
|
"backup_deleted": "Backup apagado",
|
||||||
"backup_restored": "Backup restaurado",
|
"backup_restored": "Backup restaurado",
|
||||||
"see_all_achievements": "Ver todas as conquistas",
|
"see_all_achievements": "Ver todas as conquistas",
|
||||||
|
|||||||
@@ -135,7 +135,11 @@
|
|||||||
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
||||||
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
||||||
"save_changes": "Salvează modificările",
|
"save_changes": "Salvează modificările",
|
||||||
"changes_saved": "Modificările au fost salvate cu succes"
|
"changes_saved": "Modificările au fost salvate cu succes",
|
||||||
|
"enable_all_debrid": "Activează All-Debrid",
|
||||||
|
"all_debrid_description": "All-Debrid este un descărcător fără restricții care îți permite să descarci fișiere din diverse surse.",
|
||||||
|
"all_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la All-Debrid",
|
||||||
|
"all_debrid_account_linked": "Contul All-Debrid a fost conectat cu succes"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descărcare completă",
|
"download_complete": "Descărcare completă",
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "Сейчас популярно",
|
|
||||||
"start_typing": "Начинаю вводить текст...",
|
"start_typing": "Начинаю вводить текст...",
|
||||||
|
"hot": "Сейчас популярно",
|
||||||
"weekly": "📅 Лучшие игры недели",
|
"weekly": "📅 Лучшие игры недели",
|
||||||
"achievements": "🏆 Игры с достижениями"
|
"achievements": "🏆 Игры с достижениями"
|
||||||
},
|
},
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
"need_help": "Нужна помощь?",
|
"need_help": "Нужна помощь?",
|
||||||
"favorites": "Избранное",
|
"favorites": "Избранное",
|
||||||
"playable_button_title": "Показать только установленные игры.",
|
"playable_button_title": "Показать только установленные игры.",
|
||||||
|
"add_custom_game_tooltip": "Добавить пользовательскую игру",
|
||||||
|
"show_playable_only_tooltip": "Показать только доступные для игры",
|
||||||
"custom_game_modal": "Добавить пользовательскую игру",
|
"custom_game_modal": "Добавить пользовательскую игру",
|
||||||
"custom_game_modal_description": "Добавьте пользовательскую игру в библиотеку, выбрав исполняемый файл",
|
"custom_game_modal_description": "Добавьте пользовательскую игру в библиотеку, выбрав исполняемый файл",
|
||||||
"custom_game_modal_executable_path": "Путь к исполняемому файлу",
|
"custom_game_modal_executable_path": "Путь к исполняемому файлу",
|
||||||
@@ -67,7 +69,26 @@
|
|||||||
"edit_game_modal_image_filter": "Изображение",
|
"edit_game_modal_image_filter": "Изображение",
|
||||||
"edit_game_modal_icon_resolution": "Рекомендуемое разрешение: 256x256px",
|
"edit_game_modal_icon_resolution": "Рекомендуемое разрешение: 256x256px",
|
||||||
"edit_game_modal_logo_resolution": "Рекомендуемое разрешение: 640x360px",
|
"edit_game_modal_logo_resolution": "Рекомендуемое разрешение: 640x360px",
|
||||||
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px"
|
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px",
|
||||||
|
"edit_game_modal_assets": "Ресурсы",
|
||||||
|
"edit_game_modal_drop_icon_image_here": "Перетащите изображение иконки сюда",
|
||||||
|
"edit_game_modal_drop_logo_image_here": "Перетащите изображение логотипа сюда",
|
||||||
|
"edit_game_modal_drop_hero_image_here": "Перетащите изображение обложки сюда",
|
||||||
|
"edit_game_modal_drop_to_replace_icon": "Перетащите для замены иконки",
|
||||||
|
"edit_game_modal_drop_to_replace_logo": "Перетащите для замены логотипа",
|
||||||
|
"edit_game_modal_drop_to_replace_hero": "Перетащите для замены обложки",
|
||||||
|
"install_decky_plugin": "Установить плагин Decky",
|
||||||
|
"update_decky_plugin": "Обновить плагин Decky",
|
||||||
|
"decky_plugin_installed_version": "Плагин Decky (v{{version}})",
|
||||||
|
"install_decky_plugin_title": "Установить плагин Hydra Decky",
|
||||||
|
"install_decky_plugin_message": "Это загрузит и установит плагин Hydra для Decky Loader. Может потребоваться повышенные разрешения. Продолжить?",
|
||||||
|
"update_decky_plugin_title": "Обновить плагин Hydra Decky",
|
||||||
|
"update_decky_plugin_message": "Доступна новая версия плагина Hydra Decky. Хотите обновить его сейчас?",
|
||||||
|
"decky_plugin_installed": "Плагин Decky v{{version}} успешно установлен",
|
||||||
|
"decky_plugin_installation_failed": "Не удалось установить плагин Decky: {{error}}",
|
||||||
|
"decky_plugin_installation_error": "Ошибка установки плагина Decky: {{error}}",
|
||||||
|
"confirm": "Подтвердить",
|
||||||
|
"cancel": "Отмена"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
@@ -128,6 +149,7 @@
|
|||||||
"amount_minutes": "{{amount}} минут",
|
"amount_minutes": "{{amount}} минут",
|
||||||
"accuracy": "точность {{accuracy}}%",
|
"accuracy": "точность {{accuracy}}%",
|
||||||
"add_to_library": "Добавить в библиотеку",
|
"add_to_library": "Добавить в библиотеку",
|
||||||
|
"already_in_library": "Уже в библиотеке",
|
||||||
"remove_from_library": "Удалить из библиотеки",
|
"remove_from_library": "Удалить из библиотеки",
|
||||||
"no_downloads": "Нет доступных источников",
|
"no_downloads": "Нет доступных источников",
|
||||||
"play_time": "Сыграно {{amount}}",
|
"play_time": "Сыграно {{amount}}",
|
||||||
@@ -156,11 +178,13 @@
|
|||||||
"open_folder": "Открыть папку",
|
"open_folder": "Открыть папку",
|
||||||
"open_download_location": "Просмотреть папку загрузок",
|
"open_download_location": "Просмотреть папку загрузок",
|
||||||
"create_shortcut": "Создать ярлык на рабочем столе",
|
"create_shortcut": "Создать ярлык на рабочем столе",
|
||||||
|
"create_shortcut_simple": "Создать ярлык",
|
||||||
"clear": "Очистить",
|
"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": "Настройки",
|
||||||
|
"properties": "Свойства",
|
||||||
"executable_section_title": "Файл",
|
"executable_section_title": "Файл",
|
||||||
"executable_section_description": "Путь к файлу, который будет запущен при нажатии на \"Play\"",
|
"executable_section_description": "Путь к файлу, который будет запущен при нажатии на \"Play\"",
|
||||||
"downloads_section_title": "Загрузки",
|
"downloads_section_title": "Загрузки",
|
||||||
@@ -170,22 +194,71 @@
|
|||||||
"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": "Ярлык создан",
|
||||||
|
"you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения",
|
||||||
"create_shortcut_error": "Не удалось создать ярлык",
|
"create_shortcut_error": "Не удалось создать ярлык",
|
||||||
"allow_nsfw_content": "Продолжить",
|
"add_to_favorites": "Добавить в избранное",
|
||||||
"download": "Скачать",
|
"remove_from_favorites": "Удалить из избранного",
|
||||||
"download_count": "Загрузки",
|
"failed_update_favorites": "Не удалось обновить избранное",
|
||||||
"download_error": "Этот вариант загрузки недоступен",
|
"game_removed_from_library": "Игра удалена из библиотеки",
|
||||||
"executable_path_in_use": "Исполняемый файл уже используется \"{{game}}\"",
|
"failed_remove_from_library": "Не удалось удалить из библиотеки",
|
||||||
"nsfw_content_description": "{{title}} содержит контент, который может не подходить для всех возрастов. \nВы уверены, что хотите продолжить?",
|
"files_removed_success": "Файлы успешно удалены",
|
||||||
|
"failed_remove_files": "Не удалось удалить файлы",
|
||||||
"nsfw_content_title": "Эта игра содержит неприемлемый контент",
|
"nsfw_content_title": "Эта игра содержит неприемлемый контент",
|
||||||
|
"nsfw_content_description": "{{title}} содержит контент, который может не подходить для всех возрастов. \nВы уверены, что хотите продолжить?",
|
||||||
|
"allow_nsfw_content": "Продолжить",
|
||||||
"refuse_nsfw_content": "Назад",
|
"refuse_nsfw_content": "Назад",
|
||||||
"stats": "Статистика",
|
"stats": "Статистика",
|
||||||
|
"download_count": "Загрузки",
|
||||||
"player_count": "Активные игроки",
|
"player_count": "Активные игроки",
|
||||||
|
"download_error": "Этот вариант загрузки недоступен",
|
||||||
|
"download": "Скачать",
|
||||||
|
"executable_path_in_use": "Исполняемый файл уже используется \"{{game}}\"",
|
||||||
"warning": "Внимание:",
|
"warning": "Внимание:",
|
||||||
"hydra_needs_to_remain_open": "Для этой загрузки Hydra должна оставаться открытой до завершения. Если Hydra закроется до завершения, вы потеряете прогресс.",
|
"hydra_needs_to_remain_open": "Для этой загрузки Hydra должна оставаться открытой до завершения. Если Hydra закроется до завершения, вы потеряете прогресс.",
|
||||||
"achievements": "Достижения",
|
"achievements": "Достижения",
|
||||||
"achievements_count": "Достижения {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "Достижения {{unlockedCount}}/{{achievementsCount}}",
|
||||||
|
"show_more": "Показать больше",
|
||||||
|
"show_less": "Показать меньше",
|
||||||
|
"reviews": "Отзывы",
|
||||||
|
"leave_a_review": "Оставить отзыв",
|
||||||
|
"write_review_placeholder": "Поделитесь своими мыслями об этой игре...",
|
||||||
|
"sort_newest": "Сначала новые",
|
||||||
|
"no_reviews_yet": "Пока нет отзывов",
|
||||||
|
"be_first_to_review": "Станьте первым, кто поделится своими мыслями об этой игре!",
|
||||||
|
"sort_oldest": "Сначала старые",
|
||||||
|
"sort_highest_score": "Высший балл",
|
||||||
|
"sort_lowest_score": "Низший балл",
|
||||||
|
"sort_most_voted": "Самые популярные",
|
||||||
|
"rating": "Оценка",
|
||||||
|
"rating_stats": "Оценка",
|
||||||
|
"rating_very_negative": "Очень негативно",
|
||||||
|
"rating_negative": "Негативно",
|
||||||
|
"rating_neutral": "Нейтрально",
|
||||||
|
"rating_positive": "Позитивно",
|
||||||
|
"rating_very_positive": "Очень позитивно",
|
||||||
|
"submit_review": "Отправить",
|
||||||
|
"submitting": "Отправка...",
|
||||||
|
"review_submitted_successfully": "Отзыв успешно отправлен!",
|
||||||
|
"review_submission_failed": "Не удалось отправить отзыв. Пожалуйста, попробуйте снова.",
|
||||||
|
"review_cannot_be_empty": "Текстовое поле отзыва не может быть пустым.",
|
||||||
|
"review_deleted_successfully": "Отзыв успешно удален.",
|
||||||
|
"review_deletion_failed": "Не удалось удалить отзыв. Пожалуйста, попробуйте снова.",
|
||||||
|
"loading_reviews": "Загрузка отзывов...",
|
||||||
|
"loading_more_reviews": "Загрузка дополнительных отзывов...",
|
||||||
|
"load_more_reviews": "Загрузить больше отзывов",
|
||||||
|
"you_seemed_to_enjoy_this_game": "Похоже, вам понравилась эта игра",
|
||||||
|
"would_you_recommend_this_game": "Хотите оставить отзыв об этой игре?",
|
||||||
|
"yes": "Да",
|
||||||
|
"maybe_later": "Возможно позже",
|
||||||
|
"rating_count": "Оценка",
|
||||||
|
"delete_review": "Удалить отзыв",
|
||||||
|
"remove_review": "Удалить отзыв",
|
||||||
|
"delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?",
|
||||||
|
"delete_review_modal_description": "Это действие нельзя отменить.",
|
||||||
|
"delete_review_modal_delete_button": "Удалить",
|
||||||
|
"delete_review_modal_cancel_button": "Отмена",
|
||||||
"cloud_save": "Облачное сохранение",
|
"cloud_save": "Облачное сохранение",
|
||||||
"cloud_save_description": "Сохраняйте ваш прогресс в облаке и продолжайте играть на любом устройстве",
|
"cloud_save_description": "Сохраняйте ваш прогресс в облаке и продолжайте играть на любом устройстве",
|
||||||
"backups": "Резервные копии",
|
"backups": "Резервные копии",
|
||||||
@@ -198,6 +271,7 @@
|
|||||||
"uploading_backup": "Загрузка резервной копии…",
|
"uploading_backup": "Загрузка резервной копии…",
|
||||||
"no_backups": "Вы еще не создали резервных копий для этой игры",
|
"no_backups": "Вы еще не создали резервных копий для этой игры",
|
||||||
"backup_uploaded": "Резервная копия загружена",
|
"backup_uploaded": "Резервная копия загружена",
|
||||||
|
"backup_failed": "Ошибка резервного копирования",
|
||||||
"backup_deleted": "Резервная копия удалена",
|
"backup_deleted": "Резервная копия удалена",
|
||||||
"backup_restored": "Резервная копия восстановлена",
|
"backup_restored": "Резервная копия восстановлена",
|
||||||
"see_all_achievements": "Просмотреть все достижения",
|
"see_all_achievements": "Просмотреть все достижения",
|
||||||
@@ -237,26 +311,29 @@
|
|||||||
"update_playtime_title": "Обновить время игры",
|
"update_playtime_title": "Обновить время игры",
|
||||||
"update_playtime_description": "Вручную обновите время игры для {{game}}",
|
"update_playtime_description": "Вручную обновите время игры для {{game}}",
|
||||||
"update_playtime": "Обновить время игры",
|
"update_playtime": "Обновить время игры",
|
||||||
|
"update_playtime_success": "Время игры успешно обновлено",
|
||||||
|
"update_playtime_error": "Не удалось обновить время игры",
|
||||||
"update_game_playtime": "Обновить время игры",
|
"update_game_playtime": "Обновить время игры",
|
||||||
|
"manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную. Это действие нельзя отменить.",
|
||||||
|
"manual_playtime_tooltip": "Это время игры было обновлено вручную",
|
||||||
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
|
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
|
||||||
"game_added_to_favorites": "Игра добавлена в избранное",
|
"download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.",
|
||||||
"game_removed_from_favorites": "Игра удалена из избранного",
|
"game_removed_from_favorites": "Игра удалена из избранного",
|
||||||
|
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||||
|
"game_removed_from_pinned": "Игра удалена из закрепленных",
|
||||||
|
"game_added_to_pinned": "Игра добавлена в закрепленные",
|
||||||
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов",
|
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов",
|
||||||
"create_steam_shortcut": "Создать ярлык Steam",
|
|
||||||
"you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения",
|
|
||||||
"create_start_menu_shortcut": "Создать ярлык в меню «Пуск»",
|
"create_start_menu_shortcut": "Создать ярлык в меню «Пуск»",
|
||||||
"invalid_wine_prefix_path": "Недопустимый путь префикса Wine",
|
"invalid_wine_prefix_path": "Недопустимый путь префикса Wine",
|
||||||
"invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.",
|
"invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.",
|
||||||
"missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux",
|
"missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux",
|
||||||
"download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.",
|
|
||||||
"update_playtime_success": "Время игры успешно обновлено",
|
|
||||||
"update_playtime_error": "Не удалось обновить время игры",
|
|
||||||
"manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную. Это действие нельзя отменить.",
|
|
||||||
"artifact_renamed": "Резервная копия успешно переименована",
|
"artifact_renamed": "Резервная копия успешно переименована",
|
||||||
"rename_artifact": "Переименовать резервную копию",
|
"rename_artifact": "Переименовать резервную копию",
|
||||||
"rename_artifact_description": "Переименуйте резервную копию, присвоив ей более описательное имя.",
|
"rename_artifact_description": "Переименуйте резервную копию, присвоив ей более описательное имя.",
|
||||||
"artifact_name_label": "Название резервной копии",
|
"artifact_name_label": "Название резервной копии",
|
||||||
"artifact_name_placeholder": "Введите название для резервной копии",
|
"artifact_name_placeholder": "Введите название для резервной копии",
|
||||||
|
"save_changes": "Сохранить изменения",
|
||||||
|
"required_field": "Это поле обязательно к заполнению",
|
||||||
"max_length_field": "Это поле должно содержать менее {{length}} символов",
|
"max_length_field": "Это поле должно содержать менее {{length}} символов",
|
||||||
"freeze_backup": "Закрепить, чтобы она не была перезаписана автоматическими резервными копиями",
|
"freeze_backup": "Закрепить, чтобы она не была перезаписана автоматическими резервными копиями",
|
||||||
"unfreeze_backup": "Открепить",
|
"unfreeze_backup": "Открепить",
|
||||||
@@ -264,7 +341,22 @@
|
|||||||
"backup_unfrozen": "Резервная копия откреплена",
|
"backup_unfrozen": "Резервная копия откреплена",
|
||||||
"backup_freeze_failed": "Не удалось закрепить резервную копию",
|
"backup_freeze_failed": "Не удалось закрепить резервную копию",
|
||||||
"backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий",
|
"backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий",
|
||||||
"manual_playtime_tooltip": "Это время игры было обновлено вручную"
|
"edit_game_modal_button": "Изменить детали игры",
|
||||||
|
"game_details": "Детали игры",
|
||||||
|
"currency_symbol": "₽",
|
||||||
|
"currency_country": "ru",
|
||||||
|
"prices": "Цены",
|
||||||
|
"no_prices_found": "Цены не найдены",
|
||||||
|
"view_all_prices": "Нажмите, чтобы посмотреть все цены",
|
||||||
|
"retail_price": "Розничная цена",
|
||||||
|
"keyshop_price": "Цена в магазине ключей",
|
||||||
|
"historical_retail": "Исторические розничные цены",
|
||||||
|
"historical_keyshop": "Исторические цены в магазинах ключей",
|
||||||
|
"language": "Язык",
|
||||||
|
"caption": "Субтитры",
|
||||||
|
"audio": "Аудио",
|
||||||
|
"filter_by_source": "Фильтр по источнику",
|
||||||
|
"no_repacks_found": "Источники для этой игры не найдены"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Активировать Hydra",
|
"title": "Активировать Hydra",
|
||||||
@@ -302,6 +394,7 @@
|
|||||||
"stop_seeding": "Остановить раздачу",
|
"stop_seeding": "Остановить раздачу",
|
||||||
"resume_seeding": "Продолжить раздачу",
|
"resume_seeding": "Продолжить раздачу",
|
||||||
"options": "Управлять",
|
"options": "Управлять",
|
||||||
|
"alldebrid_size_not_supported": "Информация о загрузке для AllDebrid пока не поддерживается",
|
||||||
"extract": "Распаковать файлы",
|
"extract": "Распаковать файлы",
|
||||||
"extracting": "Распаковка файлов…"
|
"extracting": "Распаковка файлов…"
|
||||||
},
|
},
|
||||||
@@ -310,13 +403,10 @@
|
|||||||
"change": "Изменить",
|
"change": "Изменить",
|
||||||
"notifications": "Уведомления",
|
"notifications": "Уведомления",
|
||||||
"enable_download_notifications": "По завершении загрузки",
|
"enable_download_notifications": "По завершении загрузки",
|
||||||
"enable_achievement_notifications": "Когда достижение разблокировано",
|
|
||||||
"enable_repack_list_notifications": "При добавлении нового репака",
|
"enable_repack_list_notifications": "При добавлении нового репака",
|
||||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||||
"quit_app_instead_hiding": "Закрывать приложение вместо сворачивания в трей",
|
"quit_app_instead_hiding": "Закрывать приложение вместо сворачивания в трей",
|
||||||
"launch_with_system": "Запускать Hydra вместе с системой",
|
"launch_with_system": "Запускать Hydra вместе с системой",
|
||||||
"launch_minimized": "Запустить Hydra в свернутом виде",
|
|
||||||
"disable_nsfw_alert": "Отключить предупреждение о непристойном контенте",
|
|
||||||
"general": "Основные",
|
"general": "Основные",
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Источники загрузки",
|
"download_sources": "Источники загрузки",
|
||||||
@@ -343,11 +433,11 @@
|
|||||||
"download_source_errored": "Ошибка",
|
"download_source_errored": "Ошибка",
|
||||||
"sync_download_sources": "Обновить источники",
|
"sync_download_sources": "Обновить источники",
|
||||||
"removed_download_source": "Источник удален",
|
"removed_download_source": "Источник удален",
|
||||||
|
"removed_download_sources": "Источники удалены",
|
||||||
"cancel_button_confirmation_delete_all_sources": "Нет",
|
"cancel_button_confirmation_delete_all_sources": "Нет",
|
||||||
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
||||||
"description_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": "Все источники обновлены",
|
||||||
@@ -356,17 +446,20 @@
|
|||||||
"found_download_option_one": "Найден {{countFormatted}} вариант загрузки",
|
"found_download_option_one": "Найден {{countFormatted}} вариант загрузки",
|
||||||
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
||||||
"import": "Импортировать",
|
"import": "Импортировать",
|
||||||
"blocked_users": "Заблокированные пользователи",
|
"public": "Публичный",
|
||||||
"friends_only": "Только для друзей",
|
|
||||||
"must_be_valid_url": "У источника должен быть правильный URL",
|
|
||||||
"privacy": "Конфиденциальность",
|
|
||||||
"private": "Частный",
|
"private": "Частный",
|
||||||
|
"friends_only": "Только для друзей",
|
||||||
|
"privacy": "Конфиденциальность",
|
||||||
"profile_visibility": "Видимость профиля",
|
"profile_visibility": "Видимость профиля",
|
||||||
"profile_visibility_description": "Выберите, кто может видеть ваш профиль и библиотеку",
|
"profile_visibility_description": "Выберите, кто может видеть ваш профиль и библиотеку",
|
||||||
"public": "Публичный",
|
|
||||||
"required_field": "Это поле обязательно к заполнению",
|
"required_field": "Это поле обязательно к заполнению",
|
||||||
"source_already_exists": "Этот источник уже добавлен",
|
"source_already_exists": "Этот источник уже добавлен",
|
||||||
|
"must_be_valid_url": "У источника должен быть правильный URL",
|
||||||
|
"blocked_users": "Заблокированные пользователи",
|
||||||
"user_unblocked": "Пользователь разблокирован",
|
"user_unblocked": "Пользователь разблокирован",
|
||||||
|
"enable_achievement_notifications": "Когда достижение разблокировано",
|
||||||
|
"launch_minimized": "Запускать Hydra в свернутом виде",
|
||||||
|
"disable_nsfw_alert": "Отключить предупреждение о непристойном контенте",
|
||||||
"seed_after_download_complete": "Раздавать после завершения загрузки",
|
"seed_after_download_complete": "Раздавать после завершения загрузки",
|
||||||
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
|
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
|
||||||
"account": "Аккаунт",
|
"account": "Аккаунт",
|
||||||
@@ -405,12 +498,25 @@
|
|||||||
"delete_theme_description": "Это приведет к удалению темы {{theme}}",
|
"delete_theme_description": "Это приведет к удалению темы {{theme}}",
|
||||||
"cancel": "Отменить",
|
"cancel": "Отменить",
|
||||||
"appearance": "Внешний вид",
|
"appearance": "Внешний вид",
|
||||||
|
"debrid": "Debrid",
|
||||||
|
"debrid_description": "Сервисы Debrid - это премиум-загрузчики без ограничений, которые позволяют быстро скачивать файлы с различных файлообменников, ограничиваясь только скоростью вашего интернета.",
|
||||||
"enable_torbox": "Включить TorBox",
|
"enable_torbox": "Включить TorBox",
|
||||||
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
|
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
|
||||||
"torbox_account_linked": "Аккаунт TorBox привязан",
|
"torbox_account_linked": "Аккаунт TorBox привязан",
|
||||||
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
|
|
||||||
"create_real_debrid_account": "Нажмите здесь, если у вас еще нет аккаунта Real-Debrid",
|
"create_real_debrid_account": "Нажмите здесь, если у вас еще нет аккаунта Real-Debrid",
|
||||||
"create_torbox_account": "Нажмите здесь, если у вас еще нет учетной записи TorBox",
|
"create_torbox_account": "Нажмите здесь, если у вас еще нет учетной записи TorBox",
|
||||||
|
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
|
||||||
|
"enable_all_debrid": "Включить All-Debrid",
|
||||||
|
"all_debrid_description": "All-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы из различных источников.",
|
||||||
|
"all_debrid_free_account_error": "Аккаунт \"{{username}}\" является бесплатным. Пожалуйста, оформите подписку на All-Debrid",
|
||||||
|
"all_debrid_account_linked": "Аккаунт All-Debrid успешно привязан",
|
||||||
|
"alldebrid_missing_key": "Пожалуйста, предоставьте API ключ",
|
||||||
|
"alldebrid_invalid_key": "Неверный API ключ",
|
||||||
|
"alldebrid_blocked": "Ваш API ключ заблокирован по геолокации или IP",
|
||||||
|
"alldebrid_banned": "Этот аккаунт был заблокирован",
|
||||||
|
"alldebrid_unknown_error": "Произошла неизвестная ошибка",
|
||||||
|
"alldebrid_invalid_response": "Неверный ответ от All-Debrid",
|
||||||
|
"alldebrid_network_error": "Ошибка сети. Пожалуйста, проверьте соединение",
|
||||||
"name_min_length": "Название темы должно содержать не менее 3 символов",
|
"name_min_length": "Название темы должно содержать не менее 3 символов",
|
||||||
"import_theme": "Импортировать тему",
|
"import_theme": "Импортировать тему",
|
||||||
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
|
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
|
||||||
@@ -424,6 +530,7 @@
|
|||||||
"installing_common_redist": "Установка…",
|
"installing_common_redist": "Установка…",
|
||||||
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
|
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
|
||||||
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
|
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
|
||||||
|
"enable_steam_achievements": "Включить поиск достижений Steam",
|
||||||
"achievement_custom_notification_position": "Позиция уведомлений достижений",
|
"achievement_custom_notification_position": "Позиция уведомлений достижений",
|
||||||
"top-left": "Верхний левый угол",
|
"top-left": "Верхний левый угол",
|
||||||
"top-center": "Верхний центр",
|
"top-center": "Верхний центр",
|
||||||
@@ -440,8 +547,7 @@
|
|||||||
"hidden": "Скрытый",
|
"hidden": "Скрытый",
|
||||||
"test_notification": "Тестовое уведомление",
|
"test_notification": "Тестовое уведомление",
|
||||||
"notification_preview": "Предварительный просмотр уведомления о достижении",
|
"notification_preview": "Предварительный просмотр уведомления о достижении",
|
||||||
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
|
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру"
|
||||||
"enable_steam_achievements": "Включить поиск достижений Steam"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Загрузка завершена",
|
"download_complete": "Загрузка завершена",
|
||||||
@@ -453,13 +559,13 @@
|
|||||||
"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": "Новый запрос на добавление в друзья",
|
"new_friend_request_title": "Новый запрос на добавление в друзья",
|
||||||
"extraction_complete": "Распаковка завершена",
|
"extraction_complete": "Распаковка завершена",
|
||||||
"game_extracted": "{{title}} успешно распакован",
|
"game_extracted": "{{title}} успешно распакован",
|
||||||
"friend_started_playing_game": "{{displayName}} начал играть в игру",
|
"friend_started_playing_game": "{{displayName}} начал играть в игру",
|
||||||
"test_achievement_notification_title": "Это тестовое уведомление",
|
"test_achievement_notification_title": "Это тестовое уведомление",
|
||||||
"test_achievement_notification_description": "Довольно круто, да?",
|
"test_achievement_notification_description": "Довольно круто, да?"
|
||||||
"new_friend_request_description": "{{displayName}} отправил вам запрос в друзья"
|
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Открыть Hydra",
|
"open": "Открыть Hydra",
|
||||||
@@ -468,7 +574,8 @@
|
|||||||
"game_card": {
|
"game_card": {
|
||||||
"available_one": "Доступный",
|
"available_one": "Доступный",
|
||||||
"available_other": "Доступный",
|
"available_other": "Доступный",
|
||||||
"no_downloads": "Нет доступных источников"
|
"no_downloads": "Нет доступных источников",
|
||||||
|
"calculating": "Вычисление"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Программы не установлены",
|
"title": "Программы не установлены",
|
||||||
@@ -489,6 +596,10 @@
|
|||||||
"last_time_played": "Последняя игра {{period}}",
|
"last_time_played": "Последняя игра {{period}}",
|
||||||
"activity": "Недавняя активность",
|
"activity": "Недавняя активность",
|
||||||
"library": "Библиотека",
|
"library": "Библиотека",
|
||||||
|
"pinned": "Закрепленные",
|
||||||
|
"achievements_earned": "Заработанные достижения",
|
||||||
|
"played_recently": "Недавно сыгранные",
|
||||||
|
"playtime": "Время игры",
|
||||||
"total_play_time": "Всего сыграно",
|
"total_play_time": "Всего сыграно",
|
||||||
"manual_playtime_tooltip": "Время игры было обновлено вручную",
|
"manual_playtime_tooltip": "Время игры было обновлено вручную",
|
||||||
"no_recent_activity_title": "Хммм... Тут ничего нет",
|
"no_recent_activity_title": "Хммм... Тут ничего нет",
|
||||||
@@ -532,24 +643,24 @@
|
|||||||
"no_pending_invites": "У вас нет запросов ожидающих ответа",
|
"no_pending_invites": "У вас нет запросов ожидающих ответа",
|
||||||
"no_blocked_users": "Вы не заблокировали ни одного пользователя",
|
"no_blocked_users": "Вы не заблокировали ни одного пользователя",
|
||||||
"friend_code_copied": "Код друга скопирован",
|
"friend_code_copied": "Код друга скопирован",
|
||||||
"displayname_max_length": "Отображаемое имя должно содержать не более 50 символов.",
|
"undo_friendship_modal_text": "Это отменит вашу дружбу с {{displayName}}.",
|
||||||
"displayname_min_length": "Отображаемое имя должно содержать не менее 3 символов.",
|
|
||||||
"image_process_failure": "Сбой при обработке изображения",
|
|
||||||
"locked_profile": "Этот профиль является частным",
|
|
||||||
"privacy_hint": "Чтобы указать, кто может это видеть, перейдите в <0>Настройки</0>.",
|
"privacy_hint": "Чтобы указать, кто может это видеть, перейдите в <0>Настройки</0>.",
|
||||||
"profile_reported": "Профиль сообщил",
|
"locked_profile": "Этот профиль является частным",
|
||||||
"report": "Отчет",
|
"image_process_failure": "Сбой при обработке изображения",
|
||||||
"report_description": "Дополнительная информация",
|
"required_field": "Это поле обязательно к заполнению",
|
||||||
"report_description_placeholder": "Дополнительная информация",
|
"displayname_min_length": "Отображаемое имя должно содержать не менее 3 символов.",
|
||||||
|
"displayname_max_length": "Отображаемое имя должно содержать не более 50 символов.",
|
||||||
"report_profile": "Пожаловаться на этот профиль",
|
"report_profile": "Пожаловаться на этот профиль",
|
||||||
"report_reason": "Почему вы жалуетесь на этот профиль?",
|
"report_reason": "Почему вы жалуетесь на этот профиль?",
|
||||||
|
"report_description": "Дополнительная информация",
|
||||||
|
"report_description_placeholder": "Дополнительная информация",
|
||||||
|
"report": "Пожаловаться",
|
||||||
"report_reason_hate": "Разжигание ненависти",
|
"report_reason_hate": "Разжигание ненависти",
|
||||||
"report_reason_other": "Другой",
|
|
||||||
"report_reason_sexual_content": "Сексуальный контент",
|
"report_reason_sexual_content": "Сексуальный контент",
|
||||||
"report_reason_spam": "Спам",
|
|
||||||
"report_reason_violence": "Насилие",
|
"report_reason_violence": "Насилие",
|
||||||
"required_field": "Это поле обязательно к заполнению",
|
"report_reason_spam": "Спам",
|
||||||
"undo_friendship_modal_text": "Это отменит вашу дружбу с {{displayName}}.",
|
"report_reason_other": "Другое",
|
||||||
|
"profile_reported": "Жалоба на профиль отправлена",
|
||||||
"your_friend_code": "Код вашего друга:",
|
"your_friend_code": "Код вашего друга:",
|
||||||
"upload_banner": "Загрузить баннер",
|
"upload_banner": "Загрузить баннер",
|
||||||
"uploading_banner": "Загрузка баннера...",
|
"uploading_banner": "Загрузка баннера...",
|
||||||
@@ -565,7 +676,12 @@
|
|||||||
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
||||||
"show_points_on_profile": "Показывать заработанные очки в своем профиле",
|
"show_points_on_profile": "Показывать заработанные очки в своем профиле",
|
||||||
"error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга",
|
"error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга",
|
||||||
"friend_code_length_error": "Код друга должен содержать 8 символов"
|
"friend_code_length_error": "Код друга должен содержать 8 символов",
|
||||||
|
"game_removed_from_pinned": "Игра удалена из закрепленных",
|
||||||
|
"game_added_to_pinned": "Игра добавлена в закрепленные",
|
||||||
|
"karma": "Карма",
|
||||||
|
"karma_count": "карма",
|
||||||
|
"karma_description": "Заработана положительными оценками отзывов"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Достижение разблокировано",
|
"achievement_unlocked": "Достижение разблокировано",
|
||||||
|
|||||||
@@ -42,3 +42,14 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
|||||||
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
|
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
|
||||||
|
|
||||||
export const MAIN_LOOP_INTERVAL = 2000;
|
export const MAIN_LOOP_INTERVAL = 2000;
|
||||||
|
|
||||||
|
export const DECKY_PLUGINS_LOCATION = path.join(
|
||||||
|
SystemPath.getPath("home"),
|
||||||
|
"homebrew",
|
||||||
|
"plugins"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const HYDRA_DECKY_PLUGIN_LOCATION = path.join(
|
||||||
|
DECKY_PLUGINS_LOCATION,
|
||||||
|
"Hydra"
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import { CatalogueCategory } from "@shared";
|
|
||||||
import { ShopAssets } from "@types";
|
|
||||||
|
|
||||||
const getCatalogue = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
category: CatalogueCategory
|
|
||||||
) => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
take: "12",
|
|
||||||
skip: "0",
|
|
||||||
});
|
|
||||||
|
|
||||||
return HydraApi.get<ShopAssets[]>(
|
|
||||||
`/catalogue/${category}?${params.toString()}`,
|
|
||||||
{},
|
|
||||||
{ needsAuth: false }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getCatalogue", getCatalogue);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { HydraApi } from "@main/services";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
|
|
||||||
const getDevelopers = async (_event: Electron.IpcMainInvokeEvent) => {
|
|
||||||
return HydraApi.get<string[]>(`/catalogue/developers`, null, {
|
|
||||||
needsAuth: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getDevelopers", getDevelopers);
|
|
||||||
51
src/main/events/catalogue/get-game-assets.ts
Normal file
51
src/main/events/catalogue/get-game-assets.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { GameShop, ShopAssets } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
|
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 8; // 8 hours
|
||||||
|
|
||||||
|
export const getGameAssets = async (objectId: string, shop: GameShop) => {
|
||||||
|
const cachedAssets = await gamesShopAssetsSublevel.get(
|
||||||
|
levelKeys.game(shop, objectId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
cachedAssets &&
|
||||||
|
cachedAssets.updatedAt + LOCAL_CACHE_EXPIRATION > Date.now()
|
||||||
|
) {
|
||||||
|
return cachedAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HydraApi.get<ShopAssets | null>(
|
||||||
|
`/games/${shop}/${objectId}/assets`,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
needsAuth: false,
|
||||||
|
}
|
||||||
|
).then(async (assets) => {
|
||||||
|
if (!assets) return null;
|
||||||
|
|
||||||
|
// Preserve existing title if it differs from the incoming title (indicating it was customized)
|
||||||
|
const shouldPreserveTitle =
|
||||||
|
cachedAssets?.title && cachedAssets.title !== assets.title;
|
||||||
|
|
||||||
|
await gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), {
|
||||||
|
...assets,
|
||||||
|
title: shouldPreserveTitle ? cachedAssets.title : assets.title,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return assets;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGameAssetsEvent = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop
|
||||||
|
) => {
|
||||||
|
return getGameAssets(objectId, shop);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getGameAssets", getGameAssetsEvent);
|
||||||
@@ -21,11 +21,9 @@ const getGameStats = async (
|
|||||||
return cachedStats;
|
return cachedStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HydraApi.get<GameStats>(
|
return HydraApi.get<GameStats>(`/games/${shop}/${objectId}/stats`, null, {
|
||||||
`/games/stats`,
|
needsAuth: false,
|
||||||
{ objectId, shop },
|
}).then(async (data) => {
|
||||||
{ needsAuth: false }
|
|
||||||
).then(async (data) => {
|
|
||||||
await gamesStatsCacheSublevel.put(levelKeys.game(shop, objectId), {
|
await gamesStatsCacheSublevel.put(levelKeys.game(shop, objectId), {
|
||||||
...data,
|
...data,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { GameShop, HowLongToBeatCategory } from "@types";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const getHowLongToBeat = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop
|
|
||||||
): Promise<HowLongToBeatCategory[] | null> => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
objectId,
|
|
||||||
shop,
|
|
||||||
});
|
|
||||||
|
|
||||||
return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, {
|
|
||||||
needsAuth: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getHowLongToBeat", getHowLongToBeat);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { HydraApi } from "@main/services";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
|
|
||||||
const getPublishers = async (_event: Electron.IpcMainInvokeEvent) => {
|
|
||||||
return HydraApi.get<string[]>(`/catalogue/publishers`, null, {
|
|
||||||
needsAuth: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getPublishers", getPublishers);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { db, levelKeys } from "@main/level";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { TrendingGame } from "@types";
|
|
||||||
|
|
||||||
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
|
||||||
const language = await db
|
|
||||||
.get<string, string>(levelKeys.language, {
|
|
||||||
valueEncoding: "utf8",
|
|
||||||
})
|
|
||||||
.then((language) => language || "en");
|
|
||||||
|
|
||||||
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
|
||||||
"/games/featured",
|
|
||||||
{ language },
|
|
||||||
{ needsAuth: false }
|
|
||||||
).catch(() => []);
|
|
||||||
|
|
||||||
return trendingGames.slice(0, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getTrendingGames", getTrendingGames);
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { GameShop, ShopAssets } from "@types";
|
|
||||||
import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
|
|
||||||
const saveGameShopAssets = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop,
|
|
||||||
assets: ShopAssets
|
|
||||||
): Promise<void> => {
|
|
||||||
const key = levelKeys.game(shop, objectId);
|
|
||||||
const existingAssets = await gamesShopAssetsSublevel.get(key);
|
|
||||||
|
|
||||||
// Preserve existing title if it differs from the incoming title (indicating it was customized)
|
|
||||||
const shouldPreserveTitle =
|
|
||||||
existingAssets?.title && existingAssets.title !== assets.title;
|
|
||||||
|
|
||||||
return gamesShopAssetsSublevel.put(key, {
|
|
||||||
...existingAssets,
|
|
||||||
...assets,
|
|
||||||
title: shouldPreserveTitle ? existingAssets.title : assets.title,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { CatalogueSearchPayload } from "@types";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const searchGames = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
payload: CatalogueSearchPayload,
|
|
||||||
take: number,
|
|
||||||
skip: number
|
|
||||||
) => {
|
|
||||||
return HydraApi.post(
|
|
||||||
"/catalogue/search",
|
|
||||||
{ ...payload, take, skip },
|
|
||||||
{ needsAuth: false }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("searchGames", searchGames);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { HydraApi } from "@main/services";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
|
|
||||||
const deleteGameArtifact = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
gameArtifactId: string
|
|
||||||
) =>
|
|
||||||
HydraApi.delete<{ ok: boolean }>(
|
|
||||||
`/profile/games/artifacts/${gameArtifactId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
registerEvent("deleteGameArtifact", deleteGameArtifact);
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { HydraApi } from "@main/services";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import type { GameArtifact, GameShop } from "@types";
|
|
||||||
import { SubscriptionRequiredError, UserNotLoggedInError } from "@shared";
|
|
||||||
|
|
||||||
const getGameArtifacts = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop
|
|
||||||
) => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
objectId,
|
|
||||||
shop,
|
|
||||||
});
|
|
||||||
|
|
||||||
return HydraApi.get<GameArtifact[]>(
|
|
||||||
`/profile/games/artifacts?${params.toString()}`,
|
|
||||||
{},
|
|
||||||
{ needsSubscription: true }
|
|
||||||
).catch((err) => {
|
|
||||||
if (err instanceof SubscriptionRequiredError) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err instanceof UserNotLoggedInError) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getGameArtifacts", getGameArtifacts);
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const renameGameArtifact = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
gameArtifactId: string,
|
|
||||||
label: string
|
|
||||||
) => {
|
|
||||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}`, {
|
|
||||||
label,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("renameGameArtifact", renameGameArtifact);
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const toggleArtifactFreeze = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
gameArtifactId: string,
|
|
||||||
freeze: boolean
|
|
||||||
) => {
|
|
||||||
if (freeze) {
|
|
||||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/freeze`);
|
|
||||||
} else {
|
|
||||||
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/unfreeze`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("toggleArtifactFreeze", toggleArtifactFreeze);
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import disk from "diskusage";
|
import { DiskUsage } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import checkDiskSpace from "check-disk-space";
|
||||||
|
|
||||||
const getDiskFreeSpace = async (
|
const getDiskFreeSpace = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
path: string
|
path: string
|
||||||
) => disk.check(path);
|
): Promise<DiskUsage> => {
|
||||||
|
const result = await checkDiskSpace(path);
|
||||||
|
return { free: result.free, total: result.size };
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("getDiskFreeSpace", getDiskFreeSpace);
|
registerEvent("getDiskFreeSpace", getDiskFreeSpace);
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants";
|
import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
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-random-game";
|
import "./catalogue/get-random-game";
|
||||||
import "./catalogue/search-games";
|
|
||||||
import "./catalogue/get-game-stats";
|
import "./catalogue/get-game-stats";
|
||||||
import "./catalogue/get-trending-games";
|
|
||||||
import "./catalogue/get-publishers";
|
|
||||||
import "./catalogue/get-developers";
|
|
||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./hardware/check-folder-write-permission";
|
import "./hardware/check-folder-write-permission";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
@@ -46,13 +39,15 @@ import "./library/copy-custom-game-asset";
|
|||||||
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";
|
||||||
import "./misc/get-features";
|
|
||||||
import "./misc/show-item-in-folder";
|
import "./misc/show-item-in-folder";
|
||||||
import "./misc/get-badges";
|
|
||||||
import "./misc/install-common-redist";
|
import "./misc/install-common-redist";
|
||||||
import "./misc/can-install-common-redist";
|
import "./misc/can-install-common-redist";
|
||||||
import "./misc/save-temp-file";
|
import "./misc/save-temp-file";
|
||||||
import "./misc/delete-temp-file";
|
import "./misc/delete-temp-file";
|
||||||
|
import "./misc/install-hydra-decky-plugin";
|
||||||
|
import "./misc/get-hydra-decky-plugin-info";
|
||||||
|
import "./misc/check-homebrew-folder-exists";
|
||||||
|
import "./misc/hydra-api-call";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
import "./torrenting/pause-game-download";
|
import "./torrenting/pause-game-download";
|
||||||
import "./torrenting/resume-game-download";
|
import "./torrenting/resume-game-download";
|
||||||
@@ -66,38 +61,23 @@ import "./user-preferences/auto-launch";
|
|||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
import "./user-preferences/authenticate-all-debrid";
|
||||||
import "./user-preferences/authenticate-torbox";
|
import "./user-preferences/authenticate-torbox";
|
||||||
import "./download-sources/put-download-source";
|
import "./download-sources/put-download-source";
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
import "./user/get-user";
|
|
||||||
import "./user/get-user-library";
|
|
||||||
import "./user/get-blocked-users";
|
|
||||||
import "./user/block-user";
|
|
||||||
import "./user/unblock-user";
|
|
||||||
import "./user/get-user-friends";
|
|
||||||
import "./user/get-auth";
|
import "./user/get-auth";
|
||||||
import "./user/get-user-stats";
|
|
||||||
import "./user/report-user";
|
|
||||||
import "./user/get-unlocked-achievements";
|
import "./user/get-unlocked-achievements";
|
||||||
import "./user/get-compared-unlocked-achievements";
|
import "./user/get-compared-unlocked-achievements";
|
||||||
import "./profile/get-friend-requests";
|
|
||||||
import "./profile/get-me";
|
import "./profile/get-me";
|
||||||
import "./profile/undo-friendship";
|
|
||||||
import "./profile/update-friend-request";
|
|
||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
import "./profile/process-profile-image";
|
import "./profile/process-profile-image";
|
||||||
import "./profile/send-friend-request";
|
|
||||||
import "./profile/sync-friend-requests";
|
import "./profile/sync-friend-requests";
|
||||||
import "./cloud-save/download-game-artifact";
|
import "./cloud-save/download-game-artifact";
|
||||||
import "./cloud-save/get-game-artifacts";
|
|
||||||
import "./cloud-save/get-game-backup-preview";
|
import "./cloud-save/get-game-backup-preview";
|
||||||
import "./cloud-save/upload-save-game";
|
import "./cloud-save/upload-save-game";
|
||||||
import "./cloud-save/delete-game-artifact";
|
|
||||||
import "./cloud-save/select-game-backup-path";
|
import "./cloud-save/select-game-backup-path";
|
||||||
import "./cloud-save/toggle-artifact-freeze";
|
|
||||||
import "./cloud-save/rename-game-artifact";
|
|
||||||
import "./notifications/publish-new-repacks-notification";
|
import "./notifications/publish-new-repacks-notification";
|
||||||
import "./notifications/update-achievement-notification-window";
|
import "./notifications/update-achievement-notification-window";
|
||||||
import "./notifications/show-achievement-test-notification";
|
import "./notifications/show-achievement-test-notification";
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const addCustomGameToLibrary = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assets = {
|
const assets = {
|
||||||
|
updatedAt: Date.now(),
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { GameShop, GameStats } from "@types";
|
import type { GameShop, ShopAssets } from "@types";
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
import {
|
import {
|
||||||
composeSteamShortcut,
|
composeSteamShortcut,
|
||||||
getSteamLocation,
|
getSteamLocation,
|
||||||
getSteamShortcuts,
|
getSteamShortcuts,
|
||||||
getSteamUsersIds,
|
getSteamUsersIds,
|
||||||
HydraApi,
|
|
||||||
logger,
|
logger,
|
||||||
SystemPath,
|
SystemPath,
|
||||||
writeSteamShortcuts,
|
writeSteamShortcuts,
|
||||||
@@ -15,6 +14,7 @@ import fs from "node:fs";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { ASSETS_PATH } from "@main/constants";
|
import { ASSETS_PATH } from "@main/constants";
|
||||||
|
import { getGameAssets } from "../catalogue/get-game-assets";
|
||||||
|
|
||||||
const downloadAsset = async (downloadPath: string, url?: string | null) => {
|
const downloadAsset = async (downloadPath: string, url?: string | null) => {
|
||||||
try {
|
try {
|
||||||
@@ -41,7 +41,7 @@ const downloadAsset = async (downloadPath: string, url?: string | null) => {
|
|||||||
const downloadAssetsFromSteam = async (
|
const downloadAssetsFromSteam = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
assets: GameStats["assets"]
|
assets: ShopAssets | null
|
||||||
) => {
|
) => {
|
||||||
const gameAssetsPath = path.join(ASSETS_PATH, `${shop}-${objectId}`);
|
const gameAssetsPath = path.join(ASSETS_PATH, `${shop}-${objectId}`);
|
||||||
|
|
||||||
@@ -86,9 +86,7 @@ const createSteamShortcut = async (
|
|||||||
throw new Error("No executable path found for game");
|
throw new Error("No executable path found for game");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { assets } = await HydraApi.get<GameStats>(
|
const assets = await getGameAssets(objectId, shop);
|
||||||
`/games/stats?objectId=${objectId}&shop=${shop}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const steamUserIds = await getSteamUsersIds();
|
const steamUserIds = await getSteamUsersIds();
|
||||||
|
|
||||||
|
|||||||
13
src/main/events/misc/check-homebrew-folder-exists.ts
Normal file
13
src/main/events/misc/check-homebrew-folder-exists.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { DECKY_PLUGINS_LOCATION } from "@main/constants";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const checkHomebrewFolderExists = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const homebrewPath = path.dirname(DECKY_PLUGINS_LOCATION);
|
||||||
|
return fs.existsSync(homebrewPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("checkHomebrewFolderExists", checkHomebrewFolderExists);
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Badge } from "@types";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import { db, levelKeys } from "@main/level";
|
|
||||||
|
|
||||||
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
|
|
||||||
const language = await db
|
|
||||||
.get<string, string>(levelKeys.language, {
|
|
||||||
valueEncoding: "utf8",
|
|
||||||
})
|
|
||||||
.then((language) => language || "en");
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
locale: language,
|
|
||||||
});
|
|
||||||
|
|
||||||
return HydraApi.get<Badge[]>(`/badges?${params.toString()}`, null, {
|
|
||||||
needsAuth: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getBadges", getBadges);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
|
|
||||||
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getFeatures", getFeatures);
|
|
||||||
94
src/main/events/misc/get-hydra-decky-plugin-info.ts
Normal file
94
src/main/events/misc/get-hydra-decky-plugin-info.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { logger, HydraApi } from "@main/services";
|
||||||
|
import { HYDRA_DECKY_PLUGIN_LOCATION } from "@main/constants";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
interface DeckyReleaseInfo {
|
||||||
|
version: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHydraDeckyPluginInfo = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
): Promise<{
|
||||||
|
installed: boolean;
|
||||||
|
version: string | null;
|
||||||
|
path: string;
|
||||||
|
outdated: boolean;
|
||||||
|
expectedVersion: string | null;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
// Fetch the expected version from API
|
||||||
|
let expectedVersion: string | null = null;
|
||||||
|
try {
|
||||||
|
const releaseInfo = await HydraApi.get<DeckyReleaseInfo>(
|
||||||
|
"/decky/release",
|
||||||
|
{},
|
||||||
|
{ needsAuth: false }
|
||||||
|
);
|
||||||
|
expectedVersion = releaseInfo.version;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to fetch Decky release info:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if plugin folder exists
|
||||||
|
if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) {
|
||||||
|
logger.log("Hydra Decky plugin not installed");
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
path: HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
outdated: true,
|
||||||
|
expectedVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if package.json exists
|
||||||
|
const packageJsonPath = path.join(
|
||||||
|
HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
"package.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
logger.log("Hydra Decky plugin package.json not found");
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
path: HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
outdated: true,
|
||||||
|
expectedVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and parse package.json
|
||||||
|
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
|
||||||
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
|
const version = packageJson.version;
|
||||||
|
|
||||||
|
const outdated = expectedVersion ? version !== expectedVersion : false;
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
`Hydra Decky plugin installed, version: ${version}, expected: ${expectedVersion}, outdated: ${outdated}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
version,
|
||||||
|
path: HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
outdated,
|
||||||
|
expectedVersion,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to get plugin info:", error);
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
path: HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
outdated: true,
|
||||||
|
expectedVersion: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getHydraDeckyPluginInfo", getHydraDeckyPluginInfo);
|
||||||
38
src/main/events/misc/hydra-api-call.ts
Normal file
38
src/main/events/misc/hydra-api-call.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
|
interface HydraApiCallPayload {
|
||||||
|
method: "get" | "post" | "put" | "patch" | "delete";
|
||||||
|
url: string;
|
||||||
|
data?: unknown;
|
||||||
|
params?: unknown;
|
||||||
|
options?: {
|
||||||
|
needsAuth?: boolean;
|
||||||
|
needsSubscription?: boolean;
|
||||||
|
ifModifiedSince?: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydraApiCall = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
payload: HydraApiCallPayload
|
||||||
|
) => {
|
||||||
|
const { method, url, data, params, options } = payload;
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case "get":
|
||||||
|
return HydraApi.get(url, params, options);
|
||||||
|
case "post":
|
||||||
|
return HydraApi.post(url, data, options);
|
||||||
|
case "put":
|
||||||
|
return HydraApi.put(url, data, options);
|
||||||
|
case "patch":
|
||||||
|
return HydraApi.patch(url, data, options);
|
||||||
|
case "delete":
|
||||||
|
return HydraApi.delete(url, options);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("hydraApiCall", hydraApiCall);
|
||||||
50
src/main/events/misc/install-hydra-decky-plugin.ts
Normal file
50
src/main/events/misc/install-hydra-decky-plugin.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { logger, DeckyPlugin } from "@main/services";
|
||||||
|
import { HYDRA_DECKY_PLUGIN_LOCATION } from "@main/constants";
|
||||||
|
|
||||||
|
const installHydraDeckyPlugin = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
path: string;
|
||||||
|
currentVersion: string | null;
|
||||||
|
expectedVersion: string;
|
||||||
|
error?: string;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
logger.log("Installing/updating Hydra Decky plugin...");
|
||||||
|
|
||||||
|
const result = await DeckyPlugin.checkPluginVersion();
|
||||||
|
|
||||||
|
if (result.exists && !result.outdated) {
|
||||||
|
logger.log("Plugin installed successfully");
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
currentVersion: result.currentVersion,
|
||||||
|
expectedVersion: result.expectedVersion,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
logger.error("Failed to install plugin");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
path: HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
currentVersion: result.currentVersion,
|
||||||
|
expectedVersion: result.expectedVersion,
|
||||||
|
error: "Plugin installation failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error("Failed to install plugin:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
path: HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
currentVersion: null,
|
||||||
|
expectedVersion: "unknown",
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("installHydraDeckyPlugin", installHydraDeckyPlugin);
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { FriendRequest } from "@types";
|
|
||||||
|
|
||||||
const getFriendRequests = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent
|
|
||||||
): Promise<FriendRequest[]> => {
|
|
||||||
return HydraApi.get(`/profile/friend-requests`).catch(() => []);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getFriendRequests", getFriendRequests);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const sendFriendRequest = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string
|
|
||||||
) => {
|
|
||||||
return HydraApi.post("/profile/friend-requests", { friendCode: userId });
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("sendFriendRequest", sendFriendRequest);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const undoFriendship = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string
|
|
||||||
) => {
|
|
||||||
await HydraApi.delete(`/profile/friends/${userId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("undoFriendship", undoFriendship);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { FriendRequestAction } from "@types";
|
|
||||||
|
|
||||||
const updateFriendRequest = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string,
|
|
||||||
action: FriendRequestAction
|
|
||||||
) => {
|
|
||||||
if (action == "CANCEL") {
|
|
||||||
return HydraApi.delete(`/profile/friend-requests/${userId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return HydraApi.patch(`/profile/friend-requests/${userId}`, {
|
|
||||||
requestState: action,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("updateFriendRequest", updateFriendRequest);
|
|
||||||
@@ -93,14 +93,9 @@ const startGameDownload = async (
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
createGame(updatedGame!).catch(() => {}),
|
createGame(updatedGame!).catch(() => {}),
|
||||||
HydraApi.post(
|
HydraApi.post(`/games/${shop}/${objectId}/download`, null, {
|
||||||
"/games/download",
|
needsAuth: false,
|
||||||
{
|
}).catch(() => {}),
|
||||||
objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
{ needsAuth: false }
|
|
||||||
).catch(() => {}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|||||||
17
src/main/events/user-preferences/authenticate-all-debrid.ts
Normal file
17
src/main/events/user-preferences/authenticate-all-debrid.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { AllDebridClient } from "@main/services/download/all-debrid";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const authenticateAllDebrid = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
apiKey: string
|
||||||
|
) => {
|
||||||
|
AllDebridClient.authorize(apiKey);
|
||||||
|
const result = await AllDebridClient.getUser();
|
||||||
|
if ("error_code" in result) {
|
||||||
|
return { error_code: result.error_code };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.user;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("authenticateAllDebrid", authenticateAllDebrid);
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const blockUser = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string
|
|
||||||
) => {
|
|
||||||
await HydraApi.post(`/users/${userId}/block`);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("blockUser", blockUser);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import { UserNotLoggedInError } from "@shared";
|
|
||||||
import type { UserBlocks } from "@types";
|
|
||||||
|
|
||||||
export const getBlockedUsers = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
take: number,
|
|
||||||
skip: number
|
|
||||||
): Promise<UserBlocks> => {
|
|
||||||
return HydraApi.get(`/profile/blocks`, { take, skip }).catch((err) => {
|
|
||||||
if (err instanceof UserNotLoggedInError) {
|
|
||||||
return { blocks: [] };
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getBlockedUsers", getBlockedUsers);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { db } from "@main/level";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { User, UserFriends } from "@types";
|
|
||||||
import { levelKeys } from "@main/level/sublevels";
|
|
||||||
|
|
||||||
export const getUserFriends = async (
|
|
||||||
userId: string,
|
|
||||||
take: number,
|
|
||||||
skip: number
|
|
||||||
): Promise<UserFriends> => {
|
|
||||||
const user = await db.get<string, User>(levelKeys.user, {
|
|
||||||
valueEncoding: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user?.id === userId) {
|
|
||||||
return HydraApi.get(`/profile/friends`, { take, skip });
|
|
||||||
}
|
|
||||||
|
|
||||||
return HydraApi.get(`/users/${userId}/friends`, { take, skip });
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserFriendsEvent = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string,
|
|
||||||
take: number,
|
|
||||||
skip: number
|
|
||||||
) => {
|
|
||||||
return getUserFriends(userId, take, skip);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getUserFriends", getUserFriendsEvent);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { UserLibraryResponse } from "@types";
|
|
||||||
|
|
||||||
const getUserLibrary = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string,
|
|
||||||
take: number = 12,
|
|
||||||
skip: number = 0,
|
|
||||||
sortBy?: string
|
|
||||||
): Promise<UserLibraryResponse | null> => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
params.append("take", take.toString());
|
|
||||||
params.append("skip", skip.toString());
|
|
||||||
|
|
||||||
if (sortBy) {
|
|
||||||
params.append("sortBy", sortBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = params.toString();
|
|
||||||
const baseUrl = `/users/${userId}/library`;
|
|
||||||
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
||||||
|
|
||||||
return HydraApi.get<UserLibraryResponse>(url).catch(() => null);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getUserLibrary", getUserLibrary);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { UserStats } from "@types";
|
|
||||||
|
|
||||||
export const getUserStats = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string
|
|
||||||
): Promise<UserStats> => {
|
|
||||||
return HydraApi.get(`/users/${userId}/stats`);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getUserStats", getUserStats);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { UserProfile } from "@types";
|
|
||||||
|
|
||||||
const getUser = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string
|
|
||||||
): Promise<UserProfile | null> => {
|
|
||||||
return HydraApi.get<UserProfile>(`/users/${userId}`).catch(() => null);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getUser", getUser);
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
export const reportUser = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string,
|
|
||||||
reason: string,
|
|
||||||
description: string
|
|
||||||
): Promise<void> => {
|
|
||||||
return HydraApi.post(`/users/${userId}/report`, {
|
|
||||||
reason,
|
|
||||||
description,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("reportUser", reportUser);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const unblockUser = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
userId: string
|
|
||||||
) => {
|
|
||||||
await HydraApi.post(`/users/${userId}/unblock`);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("unblockUser", unblockUser);
|
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
clearGamesPlaytime,
|
clearGamesPlaytime,
|
||||||
WindowManager,
|
WindowManager,
|
||||||
Lock,
|
Lock,
|
||||||
|
Aria2,
|
||||||
} from "@main/services";
|
} from "@main/services";
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
import { PythonRPC } from "./services/python-rpc";
|
import { PythonRPC } from "./services/python-rpc";
|
||||||
@@ -222,6 +223,7 @@ app.on("before-quit", async (e) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonRPC.kill();
|
PythonRPC.kill();
|
||||||
|
Aria2.kill();
|
||||||
await clearGamesPlaytime();
|
await clearGamesPlaytime();
|
||||||
canAppBeClosed = true;
|
canAppBeClosed = true;
|
||||||
app.quit();
|
app.quit();
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type { ShopAssets } from "@types";
|
|||||||
import { db } from "../level";
|
import { db } from "../level";
|
||||||
import { levelKeys } from "./keys";
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
export const gamesShopAssetsSublevel = db.sublevel<string, ShopAssets>(
|
export const gamesShopAssetsSublevel = db.sublevel<
|
||||||
levelKeys.gameShopAssets,
|
string,
|
||||||
{
|
ShopAssets & { updatedAt: number }
|
||||||
valueEncoding: "json",
|
>(levelKeys.gameShopAssets, {
|
||||||
}
|
valueEncoding: "json",
|
||||||
);
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CommonRedistManager,
|
CommonRedistManager,
|
||||||
TorBoxClient,
|
TorBoxClient,
|
||||||
RealDebridClient,
|
RealDebridClient,
|
||||||
|
AllDebridClient,
|
||||||
Aria2,
|
Aria2,
|
||||||
DownloadManager,
|
DownloadManager,
|
||||||
HydraApi,
|
HydraApi,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
startMainLoop,
|
startMainLoop,
|
||||||
Ludusavi,
|
Ludusavi,
|
||||||
Lock,
|
Lock,
|
||||||
|
DeckyPlugin,
|
||||||
} from "@main/services";
|
} from "@main/services";
|
||||||
|
|
||||||
export const loadState = async () => {
|
export const loadState = async () => {
|
||||||
@@ -37,6 +39,10 @@ export const loadState = async () => {
|
|||||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.allDebridApiKey) {
|
||||||
|
AllDebridClient.authorize(userPreferences.allDebridApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
if (userPreferences?.torBoxApiToken) {
|
if (userPreferences?.torBoxApiToken) {
|
||||||
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||||
}
|
}
|
||||||
@@ -44,6 +50,10 @@ export const loadState = async () => {
|
|||||||
Ludusavi.copyConfigFileToUserData();
|
Ludusavi.copyConfigFileToUserData();
|
||||||
Ludusavi.copyBinaryToUserData();
|
Ludusavi.copyBinaryToUserData();
|
||||||
|
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
DeckyPlugin.checkAndUpdateIfOutdated();
|
||||||
|
}
|
||||||
|
|
||||||
await HydraApi.setupApi().then(() => {
|
await HydraApi.setupApi().then(() => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
// WSClient.connect();
|
// WSClient.connect();
|
||||||
|
|||||||
@@ -45,10 +45,8 @@ export const getGameAchievementData = async (
|
|||||||
.then((language) => language || "en");
|
.then((language) => language || "en");
|
||||||
|
|
||||||
return HydraApi.get<SteamAchievement[]>(
|
return HydraApi.get<SteamAchievement[]>(
|
||||||
"/games/achievements",
|
`/games/${shop}/${objectId}/achievements`,
|
||||||
{
|
{
|
||||||
shop,
|
|
||||||
objectId,
|
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import cp from "node:child_process";
|
import cp from "node:child_process";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
export class Aria2 {
|
export class Aria2 {
|
||||||
private static process: cp.ChildProcess | null = null;
|
private static process: cp.ChildProcess | null = null;
|
||||||
@@ -23,6 +24,9 @@ export class Aria2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static kill() {
|
public static kill() {
|
||||||
this.process?.kill();
|
if (this.process) {
|
||||||
|
logger.log("Killing aria2 process");
|
||||||
|
this.process.kill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class CloudSync {
|
|||||||
try {
|
try {
|
||||||
await fs.promises.rm(backupPath, { recursive: true });
|
await fs.promises.rm(backupPath, { recursive: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to remove backup path", error);
|
logger.error("Failed to remove backup path", { backupPath, error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ export class CloudSync {
|
|||||||
try {
|
try {
|
||||||
await fs.promises.unlink(bundleLocation);
|
await fs.promises.unlink(bundleLocation);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to remove tar file", error);
|
logger.error("Failed to remove tar file", { bundleLocation, error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
400
src/main/services/decky-plugin.ts
Normal file
400
src/main/services/decky-plugin.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
import axios from "axios";
|
||||||
|
import sudo from "sudo-prompt";
|
||||||
|
import { app } from "electron";
|
||||||
|
import {
|
||||||
|
HYDRA_DECKY_PLUGIN_LOCATION,
|
||||||
|
DECKY_PLUGINS_LOCATION,
|
||||||
|
} from "@main/constants";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
import { SevenZip } from "./7zip";
|
||||||
|
import { SystemPath } from "./system-path";
|
||||||
|
import { HydraApi } from "./hydra-api";
|
||||||
|
|
||||||
|
interface DeckyReleaseInfo {
|
||||||
|
version: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeckyPlugin {
|
||||||
|
private static releaseInfo: DeckyReleaseInfo | null = null;
|
||||||
|
|
||||||
|
private static async getDeckyReleaseInfo(): Promise<DeckyReleaseInfo> {
|
||||||
|
if (this.releaseInfo) {
|
||||||
|
return this.releaseInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await HydraApi.get<DeckyReleaseInfo>(
|
||||||
|
"/decky/release",
|
||||||
|
{},
|
||||||
|
{ needsAuth: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.releaseInfo = response;
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to fetch Decky release info:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getPackageJsonPath(): string {
|
||||||
|
return path.join(HYDRA_DECKY_PLUGIN_LOCATION, "package.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async downloadPlugin(): Promise<string> {
|
||||||
|
logger.log("Downloading Hydra Decky plugin...");
|
||||||
|
|
||||||
|
const releaseInfo = await this.getDeckyReleaseInfo();
|
||||||
|
const tempDir = SystemPath.getPath("temp");
|
||||||
|
const zipPath = path.join(tempDir, "Hydra.zip");
|
||||||
|
|
||||||
|
const response = await axios.get(releaseInfo.downloadUrl, {
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.promises.writeFile(zipPath, response.data);
|
||||||
|
logger.log(`Plugin downloaded to: ${zipPath}`);
|
||||||
|
|
||||||
|
return zipPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async extractPlugin(zipPath: string): Promise<string> {
|
||||||
|
logger.log("Extracting Hydra Decky plugin...");
|
||||||
|
|
||||||
|
const tempDir = SystemPath.getPath("temp");
|
||||||
|
const extractPath = path.join(tempDir, "hydra-decky-plugin");
|
||||||
|
|
||||||
|
if (fs.existsSync(extractPath)) {
|
||||||
|
await fs.promises.rm(extractPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.mkdir(extractPath, { recursive: true });
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
SevenZip.extractFile(
|
||||||
|
{
|
||||||
|
filePath: zipPath,
|
||||||
|
outputPath: extractPath,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
logger.log(`Plugin extracted to: ${extractPath}`);
|
||||||
|
resolve(extractPath);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
reject(new Error("Failed to extract plugin"));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static needsSudo(): boolean {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(DECKY_PLUGINS_LOCATION)) {
|
||||||
|
fs.accessSync(DECKY_PLUGINS_LOCATION, fs.constants.W_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDir = path.dirname(DECKY_PLUGINS_LOCATION);
|
||||||
|
if (fs.existsSync(parentDir)) {
|
||||||
|
fs.accessSync(parentDir, fs.constants.W_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error &&
|
||||||
|
typeof error === "object" &&
|
||||||
|
"code" in error &&
|
||||||
|
(error.code === "EACCES" || error.code === "EPERM")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async installPluginWithSudo(
|
||||||
|
extractPath: string
|
||||||
|
): Promise<void> {
|
||||||
|
logger.log("Installing plugin with sudo...");
|
||||||
|
|
||||||
|
const username = os.userInfo().username;
|
||||||
|
const sourcePath = path.join(extractPath, "Hydra");
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const command = `mkdir -p "${DECKY_PLUGINS_LOCATION}" && rm -rf "${HYDRA_DECKY_PLUGIN_LOCATION}" && cp -r "${sourcePath}" "${HYDRA_DECKY_PLUGIN_LOCATION}" && chown -R ${username}: "${DECKY_PLUGINS_LOCATION}"`;
|
||||||
|
|
||||||
|
sudo.exec(
|
||||||
|
command,
|
||||||
|
{ name: app.getName() },
|
||||||
|
(sudoError, _stdout, stderr) => {
|
||||||
|
if (sudoError) {
|
||||||
|
logger.error("Failed to install plugin with sudo:", sudoError);
|
||||||
|
reject(sudoError);
|
||||||
|
} else {
|
||||||
|
logger.log("Plugin installed successfully with sudo");
|
||||||
|
if (stderr) {
|
||||||
|
logger.log("Sudo stderr:", stderr);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async installPluginWithoutSudo(
|
||||||
|
extractPath: string
|
||||||
|
): Promise<void> {
|
||||||
|
logger.log("Installing plugin without sudo...");
|
||||||
|
|
||||||
|
const sourcePath = path.join(extractPath, "Hydra");
|
||||||
|
|
||||||
|
if (!fs.existsSync(DECKY_PLUGINS_LOCATION)) {
|
||||||
|
await fs.promises.mkdir(DECKY_PLUGINS_LOCATION, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) {
|
||||||
|
await fs.promises.rm(HYDRA_DECKY_PLUGIN_LOCATION, {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.cp(sourcePath, HYDRA_DECKY_PLUGIN_LOCATION, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("Plugin installed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async installPlugin(extractPath: string): Promise<void> {
|
||||||
|
if (this.needsSudo()) {
|
||||||
|
await this.installPluginWithSudo(extractPath);
|
||||||
|
} else {
|
||||||
|
await this.installPluginWithoutSudo(extractPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async updatePlugin(): Promise<void> {
|
||||||
|
let zipPath: string | null = null;
|
||||||
|
let extractPath: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
zipPath = await this.downloadPlugin();
|
||||||
|
extractPath = await this.extractPlugin(zipPath);
|
||||||
|
await this.installPlugin(extractPath);
|
||||||
|
|
||||||
|
logger.log("Plugin update completed successfully");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to update plugin:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (zipPath) {
|
||||||
|
try {
|
||||||
|
await fs.promises.rm(zipPath, { force: true });
|
||||||
|
logger.log("Cleaned up downloaded zip file");
|
||||||
|
} catch (cleanupError) {
|
||||||
|
logger.error("Failed to clean up zip file:", cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractPath) {
|
||||||
|
try {
|
||||||
|
await fs.promises.rm(extractPath, { recursive: true, force: true });
|
||||||
|
logger.log("Cleaned up extraction directory");
|
||||||
|
} catch (cleanupError) {
|
||||||
|
logger.error(
|
||||||
|
"Failed to clean up extraction directory:",
|
||||||
|
cleanupError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async checkAndUpdateIfOutdated(): Promise<void> {
|
||||||
|
if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) {
|
||||||
|
logger.log("Hydra Decky plugin not installed, skipping update check");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJsonPath = this.getPackageJsonPath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
logger.log(
|
||||||
|
"Hydra Decky plugin package.json not found, skipping update"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseInfo = await this.getDeckyReleaseInfo();
|
||||||
|
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
|
||||||
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
const isOutdated = currentVersion !== releaseInfo.version;
|
||||||
|
|
||||||
|
if (isOutdated) {
|
||||||
|
logger.log(
|
||||||
|
`Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${releaseInfo.version}. Updating...`
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.updatePlugin();
|
||||||
|
logger.log("Hydra Decky plugin updated successfully");
|
||||||
|
} else {
|
||||||
|
logger.log(`Hydra Decky plugin is up to date (${currentVersion})`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error checking/updating Hydra Decky plugin: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async checkPluginVersion(): Promise<{
|
||||||
|
exists: boolean;
|
||||||
|
outdated: boolean;
|
||||||
|
currentVersion: string | null;
|
||||||
|
expectedVersion: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const releaseInfo = await this.getDeckyReleaseInfo();
|
||||||
|
|
||||||
|
if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) {
|
||||||
|
logger.log("Hydra Decky plugin folder not found, installing...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.updatePlugin();
|
||||||
|
|
||||||
|
// Read the actual installed version from package.json
|
||||||
|
const packageJsonPath = this.getPackageJsonPath();
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
const packageJsonContent = fs.readFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
outdated: false,
|
||||||
|
currentVersion: packageJson.version,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
outdated: false,
|
||||||
|
currentVersion: releaseInfo.version,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to install plugin:", error);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
outdated: true,
|
||||||
|
currentVersion: null,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJsonPath = this.getPackageJsonPath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
logger.log(
|
||||||
|
"Hydra Decky plugin package.json not found, installing..."
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.updatePlugin();
|
||||||
|
|
||||||
|
// Read the actual installed version from package.json
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
const packageJsonContent = fs.readFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
outdated: false,
|
||||||
|
currentVersion: packageJson.version,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
outdated: false,
|
||||||
|
currentVersion: releaseInfo.version,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
|
||||||
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
const isOutdated = currentVersion !== releaseInfo.version;
|
||||||
|
|
||||||
|
if (isOutdated) {
|
||||||
|
logger.log(
|
||||||
|
`Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${releaseInfo.version}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.updatePlugin();
|
||||||
|
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
const updatedPackageJsonContent = fs.readFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
const updatedPackageJson = JSON.parse(updatedPackageJsonContent);
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
outdated: false,
|
||||||
|
currentVersion: updatedPackageJson.version,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
outdated: false,
|
||||||
|
currentVersion: releaseInfo.version,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
logger.log(`Hydra Decky plugin is up to date (${currentVersion})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
outdated: isOutdated,
|
||||||
|
currentVersion,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error checking Hydra Decky plugin version: ${error}`);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
outdated: true,
|
||||||
|
currentVersion: null,
|
||||||
|
expectedVersion: releaseInfo.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error fetching release info: ${error}`);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
outdated: true,
|
||||||
|
currentVersion: null,
|
||||||
|
expectedVersion: "unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
315
src/main/services/download/all-debrid.ts
Normal file
315
src/main/services/download/all-debrid.ts
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import axios, { AxiosInstance } from "axios";
|
||||||
|
import type { AllDebridUser } from "@types";
|
||||||
|
import { logger } from "@main/services";
|
||||||
|
|
||||||
|
interface AllDebridMagnetStatus {
|
||||||
|
id: number;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
status: string;
|
||||||
|
statusCode: number;
|
||||||
|
downloaded: number;
|
||||||
|
uploaded: number;
|
||||||
|
seeders: number;
|
||||||
|
downloadSpeed: number;
|
||||||
|
uploadSpeed: number;
|
||||||
|
uploadDate: number;
|
||||||
|
completionDate: number;
|
||||||
|
links: Array<{
|
||||||
|
link: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllDebridError {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllDebridDownloadUrl {
|
||||||
|
link: string;
|
||||||
|
size?: number;
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AllDebridClient {
|
||||||
|
private static instance: AxiosInstance;
|
||||||
|
private static readonly baseURL = "https://api.alldebrid.com/v4";
|
||||||
|
|
||||||
|
static authorize(apiKey: string) {
|
||||||
|
logger.info("[AllDebrid] Authorizing with key:", apiKey ? "***" : "empty");
|
||||||
|
this.instance = axios.create({
|
||||||
|
baseURL: this.baseURL,
|
||||||
|
params: {
|
||||||
|
agent: "hydra",
|
||||||
|
apikey: apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUser() {
|
||||||
|
try {
|
||||||
|
const response = await this.instance.get<{
|
||||||
|
status: string;
|
||||||
|
data?: { user: AllDebridUser };
|
||||||
|
error?: AllDebridError;
|
||||||
|
}>("/user");
|
||||||
|
|
||||||
|
logger.info("[AllDebrid] API Response:", response.data);
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
const error = response.data.error;
|
||||||
|
logger.error("[AllDebrid] API Error:", error);
|
||||||
|
if (error?.code === "AUTH_MISSING_APIKEY") {
|
||||||
|
return { error_code: "alldebrid_missing_key" };
|
||||||
|
}
|
||||||
|
if (error?.code === "AUTH_BAD_APIKEY") {
|
||||||
|
return { error_code: "alldebrid_invalid_key" };
|
||||||
|
}
|
||||||
|
if (error?.code === "AUTH_BLOCKED") {
|
||||||
|
return { error_code: "alldebrid_blocked" };
|
||||||
|
}
|
||||||
|
if (error?.code === "AUTH_USER_BANNED") {
|
||||||
|
return { error_code: "alldebrid_banned" };
|
||||||
|
}
|
||||||
|
return { error_code: "alldebrid_unknown_error" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.data.data?.user) {
|
||||||
|
logger.error("[AllDebrid] No user data in response");
|
||||||
|
return { error_code: "alldebrid_invalid_response" };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Successfully got user:",
|
||||||
|
response.data.data.user.username
|
||||||
|
);
|
||||||
|
return { user: response.data.data.user };
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Request Error:", error);
|
||||||
|
if (error.response?.data?.error) {
|
||||||
|
return { error_code: "alldebrid_invalid_key" };
|
||||||
|
}
|
||||||
|
return { error_code: "alldebrid_network_error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async uploadMagnet(magnet: string) {
|
||||||
|
try {
|
||||||
|
logger.info("[AllDebrid] Uploading magnet with params:", { magnet });
|
||||||
|
|
||||||
|
const response = await this.instance.get("/magnet/upload", {
|
||||||
|
params: {
|
||||||
|
magnets: [magnet],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Upload Magnet Raw Response:",
|
||||||
|
JSON.stringify(response.data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
throw new Error(response.data.error?.message || "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const magnetInfo = response.data.data.magnets[0];
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Magnet Info:",
|
||||||
|
JSON.stringify(magnetInfo, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (magnetInfo.error) {
|
||||||
|
throw new Error(magnetInfo.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return magnetInfo.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Upload Magnet Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async checkMagnetStatus(
|
||||||
|
magnetId: number
|
||||||
|
): Promise<AllDebridMagnetStatus> {
|
||||||
|
try {
|
||||||
|
logger.info("[AllDebrid] Checking magnet status for ID:", magnetId);
|
||||||
|
|
||||||
|
const response = await this.instance.get(`/magnet/status`, {
|
||||||
|
params: {
|
||||||
|
id: magnetId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Check Magnet Status Raw Response:",
|
||||||
|
JSON.stringify(response.data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
throw new Error("No response data received");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
throw new Error(response.data.error?.message || "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificăm noua structură a răspunsului
|
||||||
|
const magnetData = response.data.data?.magnets;
|
||||||
|
if (!magnetData || typeof magnetData !== "object") {
|
||||||
|
logger.error(
|
||||||
|
"[AllDebrid] Invalid response structure:",
|
||||||
|
JSON.stringify(response.data, null, 2)
|
||||||
|
);
|
||||||
|
throw new Error("Invalid magnet status response format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertim răspunsul în formatul așteptat
|
||||||
|
const magnetStatus: AllDebridMagnetStatus = {
|
||||||
|
id: magnetData.id,
|
||||||
|
filename: magnetData.filename,
|
||||||
|
size: magnetData.size,
|
||||||
|
status: magnetData.status,
|
||||||
|
statusCode: magnetData.statusCode,
|
||||||
|
downloaded: magnetData.downloaded,
|
||||||
|
uploaded: magnetData.uploaded,
|
||||||
|
seeders: magnetData.seeders,
|
||||||
|
downloadSpeed: magnetData.downloadSpeed,
|
||||||
|
uploadSpeed: magnetData.uploadSpeed,
|
||||||
|
uploadDate: magnetData.uploadDate,
|
||||||
|
completionDate: magnetData.completionDate,
|
||||||
|
links: magnetData.links.map((link) => ({
|
||||||
|
link: link.link,
|
||||||
|
filename: link.filename,
|
||||||
|
size: link.size,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Magnet Status:",
|
||||||
|
JSON.stringify(magnetStatus, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
return magnetStatus;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Check Magnet Status Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async unlockLink(link: string) {
|
||||||
|
try {
|
||||||
|
const response = await this.instance.get<{
|
||||||
|
status: string;
|
||||||
|
data?: { link: string };
|
||||||
|
error?: AllDebridError;
|
||||||
|
}>("/link/unlock", {
|
||||||
|
params: {
|
||||||
|
link,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
throw new Error(response.data.error?.message || "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlockedLink = response.data.data?.link;
|
||||||
|
if (!unlockedLink) {
|
||||||
|
throw new Error("No download link received from AllDebrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlockedLink;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Unlock Link Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDownloadUrls(
|
||||||
|
uri: string
|
||||||
|
): Promise<AllDebridDownloadUrl[]> {
|
||||||
|
try {
|
||||||
|
logger.info("[AllDebrid] Getting download URLs for URI:", uri);
|
||||||
|
|
||||||
|
if (uri.startsWith("magnet:")) {
|
||||||
|
logger.info("[AllDebrid] Detected magnet link, uploading...");
|
||||||
|
// 1. Upload magnet
|
||||||
|
const magnetId = await this.uploadMagnet(uri);
|
||||||
|
logger.info("[AllDebrid] Magnet uploaded, ID:", magnetId);
|
||||||
|
|
||||||
|
// 2. Verificăm statusul până când avem link-uri
|
||||||
|
let retries = 0;
|
||||||
|
let magnetStatus: AllDebridMagnetStatus;
|
||||||
|
|
||||||
|
do {
|
||||||
|
magnetStatus = await this.checkMagnetStatus(magnetId);
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Magnet status:",
|
||||||
|
magnetStatus.status,
|
||||||
|
"statusCode:",
|
||||||
|
magnetStatus.statusCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (magnetStatus.statusCode === 4) {
|
||||||
|
// Ready
|
||||||
|
// Deblocăm fiecare link în parte și aruncăm eroare dacă oricare eșuează
|
||||||
|
const unlockedLinks = await Promise.all(
|
||||||
|
magnetStatus.links.map(async (link) => {
|
||||||
|
try {
|
||||||
|
const unlockedLink = await this.unlockLink(link.link);
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Successfully unlocked link:",
|
||||||
|
unlockedLink
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
link: unlockedLink,
|
||||||
|
size: link.size,
|
||||||
|
filename: link.filename,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[AllDebrid] Failed to unlock link:",
|
||||||
|
link.link,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error("Failed to unlock all links");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Got unlocked download links:",
|
||||||
|
unlockedLinks
|
||||||
|
);
|
||||||
|
console.log("[AllDebrid] FINAL LINKS →", unlockedLinks);
|
||||||
|
return unlockedLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retries++ > 30) {
|
||||||
|
// Maximum 30 de încercări
|
||||||
|
throw new Error("Timeout waiting for magnet to be ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // Așteptăm 2 secunde între verificări
|
||||||
|
} while (magnetStatus.statusCode !== 4);
|
||||||
|
} else {
|
||||||
|
logger.info("[AllDebrid] Regular link, unlocking...");
|
||||||
|
// Pentru link-uri normale, doar debridam link-ul
|
||||||
|
const downloadUrl = await this.unlockLink(uri);
|
||||||
|
logger.info("[AllDebrid] Got unlocked download URL:", downloadUrl);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
link: downloadUrl,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Get Download URLs Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return []; // Add default return for TypeScript
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
import { calculateETA, getDirSize } from "./helpers";
|
import { calculateETA, getDirSize } from "./helpers";
|
||||||
import { RealDebridClient } from "./real-debrid";
|
import { RealDebridClient } from "./real-debrid";
|
||||||
|
import { AllDebridClient } from "./all-debrid";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
@@ -40,6 +41,7 @@ export class DownloadManager {
|
|||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
downloadsToSeed?.map((download) => ({
|
downloadsToSeed?.map((download) => ({
|
||||||
|
action: "seed",
|
||||||
game_id: levelKeys.game(download.shop, download.objectId),
|
game_id: levelKeys.game(download.shop, download.objectId),
|
||||||
url: download.uri,
|
url: download.uri,
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
@@ -377,6 +379,27 @@ export class DownloadManager {
|
|||||||
allow_multiple_connections: true,
|
allow_multiple_connections: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case Downloader.AllDebrid: {
|
||||||
|
const downloadUrls = await AllDebridClient.getDownloadUrls(
|
||||||
|
download.uri
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!downloadUrls.length)
|
||||||
|
throw new Error(DownloadError.NotCachedInAllDebrid);
|
||||||
|
|
||||||
|
const totalSize = downloadUrls.reduce(
|
||||||
|
(total, url) => total + (url.size || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: "start",
|
||||||
|
game_id: downloadId,
|
||||||
|
url: downloadUrls.map((d) => d.link),
|
||||||
|
save_path: download.downloadPath,
|
||||||
|
total_size: totalSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
case Downloader.TorBox: {
|
case Downloader.TorBox: {
|
||||||
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
||||||
|
|
||||||
|
|||||||
@@ -17,17 +17,24 @@ export const calculateETA = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getDirSize = async (dir: string): Promise<number> => {
|
export const getDirSize = async (dir: string): Promise<number> => {
|
||||||
const getItemSize = async (filePath: string): Promise<number> => {
|
try {
|
||||||
const stat = await fs.promises.stat(filePath);
|
const stat = await fs.promises.stat(dir);
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
// If it's a file, return its size directly
|
||||||
return getDirSize(filePath);
|
if (!stat.isDirectory()) {
|
||||||
|
return stat.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stat.size;
|
const getItemSize = async (filePath: string): Promise<number> => {
|
||||||
};
|
const stat = await fs.promises.stat(filePath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
return getDirSize(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stat.size;
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
|
||||||
const files = await fs.promises.readdir(dir);
|
const files = await fs.promises.readdir(dir);
|
||||||
const filePaths = files.map((file) => path.join(dir, file));
|
const filePaths = files.map((file) => path.join(dir, file));
|
||||||
const sizes = await Promise.all(filePaths.map(getItemSize));
|
const sizes = await Promise.all(filePaths.map(getItemSize));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./download-manager";
|
export * from "./download-manager";
|
||||||
export * from "./real-debrid";
|
export * from "./real-debrid";
|
||||||
|
export * from "./all-debrid";
|
||||||
export * from "./torbox";
|
export * from "./torbox";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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";
|
||||||
|
|
||||||
interface HydraApiOptions {
|
export interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
needsSubscription?: boolean;
|
needsSubscription?: boolean;
|
||||||
ifModifiedSince?: Date;
|
ifModifiedSince?: Date;
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ export * from "./system-path";
|
|||||||
export * from "./library-sync";
|
export * from "./library-sync";
|
||||||
export * from "./wine";
|
export * from "./wine";
|
||||||
export * from "./lock";
|
export * from "./lock";
|
||||||
|
export * from "./decky-plugin";
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
const updatedLastTimePlayed =
|
const updatedLastTimePlayed =
|
||||||
localGame.lastTimePlayed == null ||
|
localGame.lastTimePlayed == null ||
|
||||||
(game.lastTimePlayed &&
|
(game.lastTimePlayed &&
|
||||||
new Date(game.lastTimePlayed) > localGame.lastTimePlayed)
|
new Date(game.lastTimePlayed) >
|
||||||
|
new Date(localGame.lastTimePlayed))
|
||||||
? game.lastTimePlayed
|
? game.lastTimePlayed
|
||||||
: localGame.lastTimePlayed;
|
: localGame.lastTimePlayed;
|
||||||
|
|
||||||
@@ -57,7 +58,11 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localGameShopAsset = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
|
||||||
await gamesShopAssetsSublevel.put(gameKey, {
|
await gamesShopAssetsSublevel.put(gameKey, {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
...localGameShopAsset,
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: localGame?.title || game.title, // Preserve local title if it exists
|
title: localGame?.title || game.title, // Preserve local title if it exists
|
||||||
|
|||||||
@@ -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, GameStats, UserPreferences, UserProfile } from "@types";
|
import type { Game, 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";
|
||||||
@@ -108,15 +108,14 @@ export const publishNewFriendRequestNotification = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const publishFriendStartedPlayingGameNotification = async (
|
export const publishFriendStartedPlayingGameNotification = async (
|
||||||
friend: UserProfile,
|
friend: UserProfile
|
||||||
game: GameStats
|
|
||||||
) => {
|
) => {
|
||||||
new Notification({
|
new Notification({
|
||||||
title: t("friend_started_playing_game", {
|
title: t("friend_started_playing_game", {
|
||||||
ns: "notifications",
|
ns: "notifications",
|
||||||
displayName: friend.displayName,
|
displayName: friend.displayName,
|
||||||
}),
|
}),
|
||||||
body: game.assets?.title,
|
body: friend?.currentGame?.title,
|
||||||
icon: friend?.profileImageUrl
|
icon: friend?.profileImageUrl
|
||||||
? await downloadImage(friend.profileImageUrl)
|
? await downloadImage(friend.profileImageUrl)
|
||||||
: trayIcon,
|
: trayIcon,
|
||||||
|
|||||||
@@ -79,11 +79,18 @@ const findGamePathByProcess = async (
|
|||||||
const executables = gameExecutables[gameId];
|
const executables = gameExecutables[gameId];
|
||||||
|
|
||||||
for (const executable of executables) {
|
for (const executable of executables) {
|
||||||
const pathSet = processMap.get(executable.exe);
|
const executablewithoutExtension = executable.exe.replace(/\.exe$/i, "");
|
||||||
|
|
||||||
|
const pathSet =
|
||||||
|
processMap.get(executable.exe) ??
|
||||||
|
processMap.get(executablewithoutExtension);
|
||||||
|
|
||||||
if (pathSet) {
|
if (pathSet) {
|
||||||
for (const path of pathSet) {
|
for (const path of pathSet) {
|
||||||
if (path.toLowerCase().endsWith(executable.name)) {
|
if (
|
||||||
|
path.toLowerCase().endsWith(executable.name) ||
|
||||||
|
path.toLowerCase().endsWith(executablewithoutExtension)
|
||||||
|
) {
|
||||||
const gameKey = levelKeys.game("steam", gameId);
|
const gameKey = levelKeys.game("steam", gameId);
|
||||||
const game = await gamesSublevel.get(gameKey);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
@@ -124,7 +131,6 @@ const getSystemProcessMap = async () => {
|
|||||||
if (!key || !value) return;
|
if (!key || !value) return;
|
||||||
|
|
||||||
const STEAM_COMPAT_DATA_PATH = process.environ?.STEAM_COMPAT_DATA_PATH;
|
const STEAM_COMPAT_DATA_PATH = process.environ?.STEAM_COMPAT_DATA_PATH;
|
||||||
|
|
||||||
if (STEAM_COMPAT_DATA_PATH) {
|
if (STEAM_COMPAT_DATA_PATH) {
|
||||||
winePrefixMap.set(value, STEAM_COMPAT_DATA_PATH);
|
winePrefixMap.set(value, STEAM_COMPAT_DATA_PATH);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ import { app, dialog } from "electron";
|
|||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
interface GamePayload {
|
interface GamePayload {
|
||||||
|
action: string;
|
||||||
game_id: string;
|
game_id: string;
|
||||||
url: string;
|
url: string | string[];
|
||||||
save_path: string;
|
save_path: string;
|
||||||
|
header?: string;
|
||||||
|
out?: string;
|
||||||
|
total_size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const requestSteam250 = async (path: string) => {
|
|||||||
const { window } = new JSDOM(response.data);
|
const { window } = new JSDOM(response.data);
|
||||||
const { document } = window;
|
const { document } = window;
|
||||||
|
|
||||||
return Array.from(document.querySelectorAll(".appline .title a"))
|
return Array.from(document.querySelectorAll("a[data-title]"))
|
||||||
.map(($title) => {
|
.map(($title) => {
|
||||||
const steamGameUrl = ($title as HTMLAnchorElement).href;
|
const steamGameUrl = ($title as HTMLAnchorElement).href;
|
||||||
if (!steamGameUrl) return null;
|
if (!steamGameUrl) return null;
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ export interface SteamAppDetailsResponse {
|
|||||||
|
|
||||||
export const getSteamLocation = async () => {
|
export const getSteamLocation = async () => {
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
return path.join(SystemPath.getPath("home"), ".local", "share", "Steam");
|
const possiblePaths = [
|
||||||
|
path.join(SystemPath.getPath("home"), ".steam", "steam"),
|
||||||
|
path.join(SystemPath.getPath("home"), ".local", "share", "Steam"),
|
||||||
|
];
|
||||||
|
|
||||||
|
return possiblePaths.find((p) => fs.existsSync(p)) || possiblePaths[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
@@ -76,7 +81,11 @@ export const getSteamAppDetails = async (
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error(err, { method: "getSteamAppDetails" });
|
logger.error("Error on getSteamAppDetails", {
|
||||||
|
message: err?.message,
|
||||||
|
code: err?.code,
|
||||||
|
name: err?.name,
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,58 +8,65 @@ import { levelKeys } from "@main/level/sublevels";
|
|||||||
export const getUserData = async () => {
|
export const getUserData = async () => {
|
||||||
return HydraApi.get<UserDetails>(`/profile/me`)
|
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||||
.then(async (me) => {
|
.then(async (me) => {
|
||||||
db.get<string, User>(levelKeys.user, { valueEncoding: "json" }).then(
|
try {
|
||||||
(user) => {
|
const user = await db.get<string, User>(levelKeys.user, {
|
||||||
return db.put<string, User>(
|
valueEncoding: "json",
|
||||||
levelKeys.user,
|
});
|
||||||
{
|
await db.put<string, User>(
|
||||||
...user,
|
levelKeys.user,
|
||||||
id: me.id,
|
{
|
||||||
displayName: me.displayName,
|
...user,
|
||||||
profileImageUrl: me.profileImageUrl,
|
id: me.id,
|
||||||
backgroundImageUrl: me.backgroundImageUrl,
|
displayName: me.displayName,
|
||||||
subscription: me.subscription,
|
profileImageUrl: me.profileImageUrl,
|
||||||
},
|
backgroundImageUrl: me.backgroundImageUrl,
|
||||||
{ valueEncoding: "json" }
|
subscription: me.subscription,
|
||||||
);
|
},
|
||||||
}
|
{ valueEncoding: "json" }
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to update user in DB", error);
|
||||||
|
}
|
||||||
return me;
|
return me;
|
||||||
})
|
})
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
if (err instanceof UserNotLoggedInError) {
|
if (err instanceof UserNotLoggedInError) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.error("Failed to get logged user");
|
|
||||||
|
|
||||||
const loggedUser = await db.get<string, User>(levelKeys.user, {
|
logger.error("Failed to get logged user", err);
|
||||||
valueEncoding: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loggedUser) {
|
try {
|
||||||
return {
|
const loggedUser = await db.get<string, User>(levelKeys.user, {
|
||||||
...loggedUser,
|
valueEncoding: "json",
|
||||||
username: "",
|
});
|
||||||
bio: "",
|
|
||||||
email: null,
|
if (loggedUser) {
|
||||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
return {
|
||||||
quirks: {
|
...loggedUser,
|
||||||
backupsPerGameLimit: 0,
|
username: "",
|
||||||
},
|
bio: "",
|
||||||
subscription: loggedUser.subscription
|
email: null,
|
||||||
? {
|
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||||
id: loggedUser.subscription.id,
|
quirks: {
|
||||||
status: loggedUser.subscription.status,
|
backupsPerGameLimit: 0,
|
||||||
plan: {
|
},
|
||||||
id: loggedUser.subscription.plan.id,
|
subscription: loggedUser.subscription
|
||||||
name: loggedUser.subscription.plan.name,
|
? {
|
||||||
},
|
id: loggedUser.subscription.id,
|
||||||
expiresAt: loggedUser.subscription.expiresAt,
|
status: loggedUser.subscription.status,
|
||||||
}
|
plan: {
|
||||||
: null,
|
id: loggedUser.subscription.plan.id,
|
||||||
featurebaseJwt: "",
|
name: loggedUser.subscription.plan.name,
|
||||||
} as UserDetails;
|
},
|
||||||
|
expiresAt: loggedUser.subscription.expiresAt,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
featurebaseJwt: "",
|
||||||
|
} as UserDetails;
|
||||||
|
}
|
||||||
|
} catch (dbError) {
|
||||||
|
logger.error("Failed to read user from DB", dbError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -54,20 +54,37 @@ export class WindowManager {
|
|||||||
show: false,
|
show: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static loadMainWindowURL(hash = "") {
|
private static async loadWindowURL(window: BrowserWindow, hash: string = "") {
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
this.mainWindow?.loadURL(
|
window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`);
|
||||||
`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`
|
} else if (process.env["MAIN_VITE_RENDERER_URL"]) {
|
||||||
);
|
// Try to load from remote URL in production
|
||||||
} else {
|
try {
|
||||||
this.mainWindow?.loadFile(
|
await window.loadURL(
|
||||||
path.join(__dirname, "../renderer/index.html"),
|
`${import.meta.env.MAIN_VITE_RENDERER_URL}#/${hash}`
|
||||||
{
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Fall back to local file if remote URL fails
|
||||||
|
console.error(
|
||||||
|
"Failed to load from MAIN_VITE_RENDERER_URL, falling back to local file:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
window.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||||
hash,
|
hash,
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
|
} else {
|
||||||
|
window.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||||
|
hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadMainWindowURL(hash: string = "") {
|
||||||
|
if (this.mainWindow) {
|
||||||
|
await this.loadWindowURL(this.mainWindow, hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,17 +285,8 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static loadNotificationWindowURL() {
|
private static loadNotificationWindowURL() {
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (this.notificationWindow) {
|
||||||
this.notificationWindow?.loadURL(
|
this.loadWindowURL(this.notificationWindow, "achievement-notification");
|
||||||
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.notificationWindow?.loadFile(
|
|
||||||
path.join(__dirname, "../renderer/index.html"),
|
|
||||||
{
|
|
||||||
hash: "achievement-notification",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,19 +458,10 @@ export class WindowManager {
|
|||||||
|
|
||||||
editorWindow.removeMenu();
|
editorWindow.removeMenu();
|
||||||
|
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
this.loadWindowURL(editorWindow, `theme-editor?themeId=${themeId}`);
|
||||||
editorWindow.loadURL(
|
|
||||||
`${process.env["ELECTRON_RENDERER_URL"]}#/theme-editor?themeId=${themeId}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
|
||||||
hash: `theme-editor?themeId=${themeId}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
editorWindow.once("ready-to-show", () => {
|
editorWindow.once("ready-to-show", () => {
|
||||||
editorWindow.show();
|
editorWindow.show();
|
||||||
this.mainWindow?.webContents.openDevTools();
|
|
||||||
if (!app.isPackaged || isStaging) {
|
if (!app.isPackaged || isStaging) {
|
||||||
editorWindow.webContents.openDevTools();
|
editorWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
@@ -470,12 +469,11 @@ 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") {
|
||||||
this.mainWindow?.webContents.toggleDevTools();
|
editorWindow.webContents.toggleDevTools();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
editorWindow.on("close", () => {
|
editorWindow.on("close", () => {
|
||||||
this.mainWindow?.webContents.closeDevTools();
|
|
||||||
this.editorWindows.delete(themeId);
|
this.editorWindows.delete(themeId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { FriendGameSession } from "@main/generated/envelope";
|
|||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { HydraApi } from "@main/services/hydra-api";
|
import { HydraApi } from "@main/services/hydra-api";
|
||||||
import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications";
|
import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications";
|
||||||
import type { GameStats, UserPreferences, UserProfile } from "@types";
|
import type { UserPreferences, UserProfile } from "@types";
|
||||||
|
|
||||||
export const friendGameSessionEvent = async (payload: FriendGameSession) => {
|
export const friendGameSessionEvent = async (payload: FriendGameSession) => {
|
||||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
@@ -14,14 +14,9 @@ export const friendGameSessionEvent = async (payload: FriendGameSession) => {
|
|||||||
|
|
||||||
if (userPreferences?.friendStartGameNotificationsEnabled === false) return;
|
if (userPreferences?.friendStartGameNotificationsEnabled === false) return;
|
||||||
|
|
||||||
const [friend, gameStats] = await Promise.all([
|
const friend = await HydraApi.get<UserProfile>(`/users/${payload.friendId}`);
|
||||||
HydraApi.get<UserProfile>(`/users/${payload.friendId}`),
|
|
||||||
HydraApi.get<GameStats>(
|
|
||||||
`/games/stats?objectId=${payload.objectId}&shop=steam`
|
|
||||||
),
|
|
||||||
]).catch(() => [null, null]);
|
|
||||||
|
|
||||||
if (friend && gameStats) {
|
if (friend) {
|
||||||
publishFriendStartedPlayingGameNotification(friend, gameStats);
|
publishFriendStartedPlayingGameNotification(friend);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
2
src/main/vite-env.d.ts
vendored
2
src/main/vite-env.d.ts
vendored
@@ -7,6 +7,8 @@ interface ImportMetaEnv {
|
|||||||
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;
|
readonly MAIN_VITE_WS_URL: string;
|
||||||
|
readonly MAIN_VITE_RENDERER_URL: string;
|
||||||
|
readonly ELECTRON_RENDERER_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -11,17 +11,15 @@ import type {
|
|||||||
GameRunning,
|
GameRunning,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
CatalogueSearchPayload,
|
|
||||||
SeedingStatus,
|
SeedingStatus,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
Theme,
|
Theme,
|
||||||
FriendRequestSync,
|
FriendRequestSync,
|
||||||
ShortcutLocation,
|
ShortcutLocation,
|
||||||
ShopAssets,
|
|
||||||
AchievementCustomNotificationPosition,
|
AchievementCustomNotificationPosition,
|
||||||
AchievementNotificationInfo,
|
AchievementNotificationInfo,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
import type { AuthPage } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
@@ -63,20 +61,13 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("checkDebridAvailability", magnets),
|
ipcRenderer.invoke("checkDebridAvailability", magnets),
|
||||||
|
|
||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
searchGames: (payload: CatalogueSearchPayload, take: number, skip: number) =>
|
|
||||||
ipcRenderer.invoke("searchGames", payload, take, skip),
|
|
||||||
getCatalogue: (category: CatalogueCategory) =>
|
|
||||||
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"),
|
||||||
getHowLongToBeat: (objectId: string, shop: GameShop) =>
|
|
||||||
ipcRenderer.invoke("getHowLongToBeat", objectId, shop),
|
|
||||||
getGameStats: (objectId: string, shop: GameShop) =>
|
getGameStats: (objectId: string, shop: GameShop) =>
|
||||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||||
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
getGameAssets: (objectId: string, shop: GameShop) =>
|
||||||
|
ipcRenderer.invoke("getGameAssets", objectId, shop),
|
||||||
onUpdateAchievements: (
|
onUpdateAchievements: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
@@ -102,6 +93,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
||||||
authenticateRealDebrid: (apiToken: string) =>
|
authenticateRealDebrid: (apiToken: string) =>
|
||||||
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
||||||
|
authenticateAllDebrid: (apiKey: string) =>
|
||||||
|
ipcRenderer.invoke("authenticateAllDebrid", apiKey),
|
||||||
authenticateTorBox: (apiToken: string) =>
|
authenticateTorBox: (apiToken: string) =>
|
||||||
ipcRenderer.invoke("authenticateTorBox", apiToken),
|
ipcRenderer.invoke("authenticateTorBox", apiToken),
|
||||||
|
|
||||||
@@ -284,10 +277,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
downloadOptionTitle: string | null
|
downloadOptionTitle: string | null
|
||||||
) =>
|
) =>
|
||||||
ipcRenderer.invoke("uploadSaveGame", objectId, shop, downloadOptionTitle),
|
ipcRenderer.invoke("uploadSaveGame", objectId, shop, downloadOptionTitle),
|
||||||
toggleArtifactFreeze: (gameArtifactId: string, freeze: boolean) =>
|
|
||||||
ipcRenderer.invoke("toggleArtifactFreeze", gameArtifactId, freeze),
|
|
||||||
renameGameArtifact: (gameArtifactId: string, label: string) =>
|
|
||||||
ipcRenderer.invoke("renameGameArtifact", gameArtifactId, label),
|
|
||||||
downloadGameArtifact: (
|
downloadGameArtifact: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
@@ -298,8 +287,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getGameArtifacts", objectId, shop),
|
ipcRenderer.invoke("getGameArtifacts", objectId, shop),
|
||||||
getGameBackupPreview: (objectId: string, shop: GameShop) =>
|
getGameBackupPreview: (objectId: string, shop: GameShop) =>
|
||||||
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
||||||
deleteGameArtifact: (gameArtifactId: string) =>
|
|
||||||
ipcRenderer.invoke("deleteGameArtifact", gameArtifactId),
|
|
||||||
selectGameBackupPath: (
|
selectGameBackupPath: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -356,10 +343,99 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("showOpenDialog", options),
|
ipcRenderer.invoke("showOpenDialog", options),
|
||||||
showItemInFolder: (path: string) =>
|
showItemInFolder: (path: string) =>
|
||||||
ipcRenderer.invoke("showItemInFolder", path),
|
ipcRenderer.invoke("showItemInFolder", path),
|
||||||
getFeatures: () => ipcRenderer.invoke("getFeatures"),
|
hydraApi: {
|
||||||
getBadges: () => ipcRenderer.invoke("getBadges"),
|
get: (
|
||||||
|
url: string,
|
||||||
|
options?: {
|
||||||
|
params?: unknown;
|
||||||
|
needsAuth?: boolean;
|
||||||
|
needsSubscription?: boolean;
|
||||||
|
ifModifiedSince?: Date;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke("hydraApiCall", {
|
||||||
|
method: "get",
|
||||||
|
url,
|
||||||
|
params: options?.params,
|
||||||
|
options: {
|
||||||
|
needsAuth: options?.needsAuth,
|
||||||
|
needsSubscription: options?.needsSubscription,
|
||||||
|
ifModifiedSince: options?.ifModifiedSince,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
post: (
|
||||||
|
url: string,
|
||||||
|
options?: {
|
||||||
|
data?: unknown;
|
||||||
|
needsAuth?: boolean;
|
||||||
|
needsSubscription?: boolean;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke("hydraApiCall", {
|
||||||
|
method: "post",
|
||||||
|
url,
|
||||||
|
data: options?.data,
|
||||||
|
options: {
|
||||||
|
needsAuth: options?.needsAuth,
|
||||||
|
needsSubscription: options?.needsSubscription,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
put: (
|
||||||
|
url: string,
|
||||||
|
options?: {
|
||||||
|
data?: unknown;
|
||||||
|
needsAuth?: boolean;
|
||||||
|
needsSubscription?: boolean;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke("hydraApiCall", {
|
||||||
|
method: "put",
|
||||||
|
url,
|
||||||
|
data: options?.data,
|
||||||
|
options: {
|
||||||
|
needsAuth: options?.needsAuth,
|
||||||
|
needsSubscription: options?.needsSubscription,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
patch: (
|
||||||
|
url: string,
|
||||||
|
options?: {
|
||||||
|
data?: unknown;
|
||||||
|
needsAuth?: boolean;
|
||||||
|
needsSubscription?: boolean;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke("hydraApiCall", {
|
||||||
|
method: "patch",
|
||||||
|
url,
|
||||||
|
data: options?.data,
|
||||||
|
options: {
|
||||||
|
needsAuth: options?.needsAuth,
|
||||||
|
needsSubscription: options?.needsSubscription,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
delete: (
|
||||||
|
url: string,
|
||||||
|
options?: {
|
||||||
|
needsAuth?: boolean;
|
||||||
|
needsSubscription?: boolean;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke("hydraApiCall", {
|
||||||
|
method: "delete",
|
||||||
|
url,
|
||||||
|
options: {
|
||||||
|
needsAuth: options?.needsAuth,
|
||||||
|
needsSubscription: options?.needsSubscription,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
canInstallCommonRedist: () => ipcRenderer.invoke("canInstallCommonRedist"),
|
canInstallCommonRedist: () => ipcRenderer.invoke("canInstallCommonRedist"),
|
||||||
installCommonRedist: () => ipcRenderer.invoke("installCommonRedist"),
|
installCommonRedist: () => ipcRenderer.invoke("installCommonRedist"),
|
||||||
|
installHydraDeckyPlugin: () => ipcRenderer.invoke("installHydraDeckyPlugin"),
|
||||||
|
getHydraDeckyPluginInfo: () => ipcRenderer.invoke("getHydraDeckyPluginInfo"),
|
||||||
|
checkHomebrewFolderExists: () =>
|
||||||
|
ipcRenderer.invoke("checkHomebrewFolderExists"),
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
|
||||||
/* Auto update */
|
/* Auto update */
|
||||||
@@ -390,13 +466,10 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
undoFriendship: (userId: string) =>
|
|
||||||
ipcRenderer.invoke("undoFriendship", userId),
|
|
||||||
updateProfile: (updateProfile: UpdateProfileRequest) =>
|
updateProfile: (updateProfile: UpdateProfileRequest) =>
|
||||||
ipcRenderer.invoke("updateProfile", updateProfile),
|
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||||
processProfileImage: (imagePath: string) =>
|
processProfileImage: (imagePath: string) =>
|
||||||
ipcRenderer.invoke("processProfileImage", imagePath),
|
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
|
||||||
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
|
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
|
||||||
onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => {
|
onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
@@ -409,26 +482,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
},
|
},
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||||
sendFriendRequest: (userId: string) =>
|
|
||||||
ipcRenderer.invoke("sendFriendRequest", userId),
|
|
||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
|
||||||
getUserLibrary: (
|
|
||||||
userId: string,
|
|
||||||
take?: number,
|
|
||||||
skip?: number,
|
|
||||||
sortBy?: string
|
|
||||||
) => ipcRenderer.invoke("getUserLibrary", userId, take, skip, sortBy),
|
|
||||||
blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
|
|
||||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
|
||||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
|
||||||
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
|
||||||
getBlockedUsers: (take: number, skip: number) =>
|
|
||||||
ipcRenderer.invoke("getBlockedUsers", take, skip),
|
|
||||||
getUserStats: (userId: string) => ipcRenderer.invoke("getUserStats", userId),
|
|
||||||
reportUser: (userId: string, reason: string, description: string) =>
|
|
||||||
ipcRenderer.invoke("reportUser", userId, reason, description),
|
|
||||||
getComparedUnlockedAchievements: (
|
getComparedUnlockedAchievements: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { downloadSourcesWorker } from "./workers";
|
|||||||
import { downloadSourcesTable } from "./dexie";
|
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 { generateUUID } from "./helpers";
|
||||||
|
|
||||||
import { injectCustomCss, removeCustomCss } from "./helpers";
|
import { injectCustomCss, removeCustomCss } from "./helpers";
|
||||||
import "./app.scss";
|
import "./app.scss";
|
||||||
@@ -212,7 +213,7 @@ export function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateRepacks();
|
updateRepacks();
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
const id = generateUUID();
|
||||||
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||||
|
|
||||||
channel.onmessage = async (event: MessageEvent<number>) => {
|
channel.onmessage = async (event: MessageEvent<number>) => {
|
||||||
|
|||||||
BIN
src/renderer/src/assets/icons/decky.png
Normal file
BIN
src/renderer/src/assets/icons/decky.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
@@ -8,7 +8,7 @@
|
|||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: globals.$spacing-unit;
|
||||||
}
|
}
|
||||||
&__description {
|
&__description {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function ConfirmationModal({
|
|||||||
{cancelButtonLabel}
|
{cancelButtonLabel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme="danger"
|
theme="primary"
|
||||||
disabled={buttonsIsDisabled}
|
disabled={buttonsIsDisabled}
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
>
|
>
|
||||||
|
|||||||
158
src/renderer/src/components/context-menu/context-menu.scss
Normal file
158
src/renderer/src/components/context-menu/context-menu.scss
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: 180px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 1.5);
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: globals.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
text-align: left;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(&--disabled) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(&--disabled) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
color: globals.$danger-color;
|
||||||
|
|
||||||
|
&:hover:not(.context-menu__item--disabled) {
|
||||||
|
background-color: rgba(128, 29, 30, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__item-icon {
|
||||||
|
color: globals.$danger-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--has-submenu {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item-label {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item-arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submenu {
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: 160px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
margin-left: 0;
|
||||||
|
z-index: 1200;
|
||||||
|
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
border-top: 1px solid globals.$border-color;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item + &__item {
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item--danger:first-of-type {
|
||||||
|
border-top: 1px solid globals.$border-color;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--game-not-installed &__submenu &__item--danger:first-of-type {
|
||||||
|
border-top: none;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__separator {
|
||||||
|
height: 1px;
|
||||||
|
background: globals.$border-color;
|
||||||
|
margin: 6px 8px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
animation: contextMenuFadeIn 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes contextMenuFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
254
src/renderer/src/components/context-menu/context-menu.tsx
Normal file
254
src/renderer/src/components/context-menu/context-menu.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import cn from "classnames";
|
||||||
|
import "./context-menu.scss";
|
||||||
|
|
||||||
|
export interface ContextMenuItemData {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
danger?: boolean;
|
||||||
|
separator?: boolean;
|
||||||
|
submenu?: ContextMenuItemData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuProps {
|
||||||
|
items: ContextMenuItemData[];
|
||||||
|
visible: boolean;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
onClose: () => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContextMenu({
|
||||||
|
items,
|
||||||
|
visible,
|
||||||
|
position,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: ContextMenuProps) {
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [adjustedPosition, setAdjustedPosition] = useState(position);
|
||||||
|
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
||||||
|
const submenuCloseTimeout = useRef<number | null>(null);
|
||||||
|
const itemRefs = useRef<Record<string, HTMLLIElement | null>>({});
|
||||||
|
const [submenuStyles, setSubmenuStyles] = useState<
|
||||||
|
Record<string, React.CSSProperties>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEscape = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
};
|
||||||
|
}, [visible, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible || !menuRef.current) return;
|
||||||
|
|
||||||
|
const rect = menuRef.current.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
let adjustedX = position.x;
|
||||||
|
let adjustedY = position.y;
|
||||||
|
|
||||||
|
if (position.x + rect.width > viewportWidth) {
|
||||||
|
adjustedX = viewportWidth - rect.width - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position.y + rect.height > viewportHeight) {
|
||||||
|
adjustedY = viewportHeight - rect.height - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdjustedPosition({ x: adjustedX, y: adjustedY });
|
||||||
|
}, [visible, position]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
setActiveSubmenu(null);
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
const handleItemClick = (item: ContextMenuItemData) => {
|
||||||
|
if (item.disabled) return;
|
||||||
|
|
||||||
|
if (item.submenu) {
|
||||||
|
setActiveSubmenu(activeSubmenu === item.id ? null : item.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.onClick) {
|
||||||
|
item.onClick();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmenuMouseEnter = (itemId: string) => {
|
||||||
|
if (submenuCloseTimeout.current) {
|
||||||
|
window.clearTimeout(submenuCloseTimeout.current);
|
||||||
|
submenuCloseTimeout.current = null;
|
||||||
|
}
|
||||||
|
setActiveSubmenu(itemId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmenuMouseLeave = () => {
|
||||||
|
if (submenuCloseTimeout.current) {
|
||||||
|
window.clearTimeout(submenuCloseTimeout.current);
|
||||||
|
}
|
||||||
|
submenuCloseTimeout.current = window.setTimeout(() => {
|
||||||
|
setActiveSubmenu(null);
|
||||||
|
submenuCloseTimeout.current = null;
|
||||||
|
}, 120);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeSubmenu) return;
|
||||||
|
|
||||||
|
const parentEl = itemRefs.current[activeSubmenu];
|
||||||
|
if (!parentEl) return;
|
||||||
|
|
||||||
|
const submenuEl = parentEl.querySelector(
|
||||||
|
".context-menu__submenu"
|
||||||
|
) as HTMLElement | null;
|
||||||
|
if (!submenuEl) return;
|
||||||
|
|
||||||
|
const parentRect = parentEl.getBoundingClientRect();
|
||||||
|
const submenuWidth = submenuEl.offsetWidth;
|
||||||
|
const submenuHeight = submenuEl.offsetHeight;
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
const styles: React.CSSProperties = {};
|
||||||
|
|
||||||
|
if (parentRect.right + submenuWidth > viewportWidth - 8) {
|
||||||
|
styles.left = "auto";
|
||||||
|
styles.right = "100%";
|
||||||
|
} else {
|
||||||
|
styles.left = "100%";
|
||||||
|
styles.right = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overflowBottom = parentRect.top + submenuHeight - viewportHeight;
|
||||||
|
if (overflowBottom > 0) {
|
||||||
|
const topAdjust = Math.min(overflowBottom + 8, parentRect.top - 8);
|
||||||
|
styles.top = `${-topAdjust}px`;
|
||||||
|
} else {
|
||||||
|
styles.top = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmenuStyles((prev) => ({ ...prev, [activeSubmenu]: styles }));
|
||||||
|
}, [activeSubmenu]);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
const menuContent = (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className={cn("context-menu", className)}
|
||||||
|
style={{
|
||||||
|
left: adjustedPosition.x,
|
||||||
|
top: adjustedPosition.y,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ul className="context-menu__list">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
ref={(el) => (itemRefs.current[item.id] = el)}
|
||||||
|
className="context-menu__item-container"
|
||||||
|
onMouseEnter={() =>
|
||||||
|
item.submenu && handleSubmenuMouseEnter(item.id)
|
||||||
|
}
|
||||||
|
onMouseLeave={() => item.submenu && handleSubmenuMouseLeave()}
|
||||||
|
>
|
||||||
|
{item.separator && <div className="context-menu__separator" />}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("context-menu__item", {
|
||||||
|
"context-menu__item--disabled": item.disabled,
|
||||||
|
"context-menu__item--danger": item.danger,
|
||||||
|
"context-menu__item--has-submenu": item.submenu,
|
||||||
|
"context-menu__item--active": activeSubmenu === item.id,
|
||||||
|
})}
|
||||||
|
onClick={() => handleItemClick(item)}
|
||||||
|
disabled={item.disabled}
|
||||||
|
>
|
||||||
|
{item.icon && (
|
||||||
|
<span className="context-menu__item-icon">{item.icon}</span>
|
||||||
|
)}
|
||||||
|
<span className="context-menu__item-label">{item.label}</span>
|
||||||
|
{item.submenu && (
|
||||||
|
<span className="context-menu__item-arrow">▶</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{item.submenu && activeSubmenu === item.id && (
|
||||||
|
<div
|
||||||
|
className="context-menu__submenu"
|
||||||
|
style={submenuStyles[item.id] || undefined}
|
||||||
|
onMouseEnter={() => handleSubmenuMouseEnter(item.id)}
|
||||||
|
onMouseLeave={() => handleSubmenuMouseLeave()}
|
||||||
|
>
|
||||||
|
<ul className="context-menu__list">
|
||||||
|
{item.submenu.map((subItem) => (
|
||||||
|
<li
|
||||||
|
key={subItem.id}
|
||||||
|
className="context-menu__item-container"
|
||||||
|
>
|
||||||
|
{subItem.separator && (
|
||||||
|
<div className="context-menu__separator" />
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("context-menu__item", {
|
||||||
|
"context-menu__item--disabled": subItem.disabled,
|
||||||
|
"context-menu__item--danger": subItem.danger,
|
||||||
|
})}
|
||||||
|
onClick={() => handleItemClick(subItem)}
|
||||||
|
disabled={subItem.disabled}
|
||||||
|
>
|
||||||
|
{subItem.icon && (
|
||||||
|
<span className="context-menu__item-icon">
|
||||||
|
{subItem.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="context-menu__item-label">
|
||||||
|
{subItem.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{children && <div className="context-menu__content">{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return createPortal(menuContent, document.body);
|
||||||
|
}
|
||||||
@@ -72,7 +72,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
|
|
||||||
|
.star-rating {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title-container {
|
&__title-container {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import "./game-card.scss";
|
|||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
|
import { StarRating } from "../star-rating/star-rating";
|
||||||
import { useCallback, useState, useMemo } from "react";
|
import { useCallback, useState, useMemo } from "react";
|
||||||
import { useFormat, useRepacks } from "@renderer/hooks";
|
import { useFormat, useRepacks } from "@renderer/hooks";
|
||||||
|
|
||||||
@@ -107,6 +108,14 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
|||||||
{stats ? numberFormatter.format(stats.playerCount) : "…"}
|
{stats ? numberFormatter.format(stats.playerCount) : "…"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="game-card__specifics-item">
|
||||||
|
<StarRating
|
||||||
|
rating={stats?.averageScore || null}
|
||||||
|
size={14}
|
||||||
|
showCalculating={!!(stats && stats.averageScore === null)}
|
||||||
|
calculatingText={t("calculating")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
PlayIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
HeartIcon,
|
||||||
|
HeartFillIcon,
|
||||||
|
GearIcon,
|
||||||
|
PencilIcon,
|
||||||
|
FileDirectoryIcon,
|
||||||
|
LinkIcon,
|
||||||
|
TrashIcon,
|
||||||
|
XIcon,
|
||||||
|
} from "@primer/octicons-react";
|
||||||
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
import { LibraryGame } from "@types";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuItemData,
|
||||||
|
ContextMenuProps,
|
||||||
|
ConfirmationModal,
|
||||||
|
useGameActions,
|
||||||
|
} from "..";
|
||||||
|
|
||||||
|
interface GameContextMenuProps extends Omit<ContextMenuProps, "items"> {
|
||||||
|
game: LibraryGame;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GameContextMenu({
|
||||||
|
game,
|
||||||
|
visible,
|
||||||
|
position,
|
||||||
|
onClose,
|
||||||
|
}: GameContextMenuProps) {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
const [showConfirmRemoveLibrary, setShowConfirmRemoveLibrary] =
|
||||||
|
useState(false);
|
||||||
|
const [showConfirmRemoveFiles, setShowConfirmRemoveFiles] = useState(false);
|
||||||
|
const {
|
||||||
|
canPlay,
|
||||||
|
isDeleting,
|
||||||
|
isGameDownloading,
|
||||||
|
isGameRunning,
|
||||||
|
hasRepacks,
|
||||||
|
shouldShowCreateStartMenuShortcut,
|
||||||
|
handlePlayGame,
|
||||||
|
handleCloseGame,
|
||||||
|
handleToggleFavorite,
|
||||||
|
handleCreateShortcut,
|
||||||
|
handleCreateSteamShortcut,
|
||||||
|
handleOpenFolder,
|
||||||
|
handleOpenDownloadOptions,
|
||||||
|
handleOpenDownloadLocation,
|
||||||
|
handleRemoveFromLibrary,
|
||||||
|
handleRemoveFiles,
|
||||||
|
handleOpenGameOptions,
|
||||||
|
} = useGameActions(game);
|
||||||
|
|
||||||
|
const items: ContextMenuItemData[] = [
|
||||||
|
{
|
||||||
|
id: "play",
|
||||||
|
label: isGameRunning ? t("close") : canPlay ? t("play") : t("download"),
|
||||||
|
icon: isGameRunning ? (
|
||||||
|
<XIcon size={16} />
|
||||||
|
) : canPlay ? (
|
||||||
|
<PlayIcon size={16} />
|
||||||
|
) : (
|
||||||
|
<DownloadIcon size={16} />
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
if (isGameRunning) {
|
||||||
|
void handleCloseGame();
|
||||||
|
} else {
|
||||||
|
void handlePlayGame();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "favorite",
|
||||||
|
label: game.favorite ? t("remove_from_favorites") : t("add_to_favorites"),
|
||||||
|
icon: game.favorite ? (
|
||||||
|
<HeartFillIcon size={16} />
|
||||||
|
) : (
|
||||||
|
<HeartIcon size={16} />
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
void handleToggleFavorite();
|
||||||
|
},
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
...(game.executablePath
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "shortcuts",
|
||||||
|
label: t("create_shortcut_simple"),
|
||||||
|
icon: <LinkIcon size={16} />,
|
||||||
|
disabled: isDeleting,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
id: "desktop-shortcut",
|
||||||
|
label: t("create_shortcut"),
|
||||||
|
icon: <LinkIcon size={16} />,
|
||||||
|
onClick: () => handleCreateShortcut("desktop"),
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "steam-shortcut",
|
||||||
|
label: t("create_steam_shortcut"),
|
||||||
|
icon: <SteamLogo style={{ width: 16, height: 16 }} />,
|
||||||
|
onClick: handleCreateSteamShortcut,
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
...(shouldShowCreateStartMenuShortcut
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "start-menu-shortcut",
|
||||||
|
label: t("create_start_menu_shortcut"),
|
||||||
|
icon: <LinkIcon size={16} />,
|
||||||
|
onClick: () => handleCreateShortcut("start_menu"),
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "manage",
|
||||||
|
label: t("options"),
|
||||||
|
icon: <GearIcon size={16} />,
|
||||||
|
disabled: isDeleting,
|
||||||
|
submenu: [
|
||||||
|
...(game.executablePath
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "open-folder",
|
||||||
|
label: t("open_folder"),
|
||||||
|
icon: <FileDirectoryIcon size={16} />,
|
||||||
|
onClick: handleOpenFolder,
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(game.executablePath
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "download-options",
|
||||||
|
label: t("open_download_options"),
|
||||||
|
icon: <PlayIcon size={16} />,
|
||||||
|
onClick: handleOpenDownloadOptions,
|
||||||
|
disabled: isDeleting || isGameDownloading || !hasRepacks,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(game.download?.downloadPath
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "download-location",
|
||||||
|
label: t("open_download_location"),
|
||||||
|
icon: <FileDirectoryIcon size={16} />,
|
||||||
|
onClick: handleOpenDownloadLocation,
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "remove-library",
|
||||||
|
label: t("remove_from_library"),
|
||||||
|
icon: <XIcon size={16} />,
|
||||||
|
onClick: () => setShowConfirmRemoveLibrary(true),
|
||||||
|
disabled: isDeleting,
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
...(game.download?.downloadPath
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "remove-files",
|
||||||
|
label: t("remove_files"),
|
||||||
|
icon: <TrashIcon size={16} />,
|
||||||
|
onClick: () => setShowConfirmRemoveFiles(true),
|
||||||
|
disabled: isDeleting || isGameDownloading,
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "properties",
|
||||||
|
label: t("properties"),
|
||||||
|
separator: true,
|
||||||
|
icon: <PencilIcon size={16} />,
|
||||||
|
onClick: () => handleOpenGameOptions(),
|
||||||
|
disabled: isDeleting,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ContextMenu
|
||||||
|
items={items}
|
||||||
|
visible={visible}
|
||||||
|
position={position}
|
||||||
|
onClose={onClose}
|
||||||
|
className={
|
||||||
|
!game.executablePath ? "context-menu--game-not-installed" : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
visible={showConfirmRemoveLibrary}
|
||||||
|
title={t("remove_from_library_title")}
|
||||||
|
descriptionText={t("remove_from_library_description", {
|
||||||
|
game: game.title,
|
||||||
|
})}
|
||||||
|
onClose={() => {
|
||||||
|
setShowConfirmRemoveLibrary(false);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
onConfirm={async () => {
|
||||||
|
setShowConfirmRemoveLibrary(false);
|
||||||
|
onClose();
|
||||||
|
await handleRemoveFromLibrary();
|
||||||
|
}}
|
||||||
|
cancelButtonLabel={t("cancel")}
|
||||||
|
confirmButtonLabel={t("remove")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
visible={showConfirmRemoveFiles}
|
||||||
|
title={t("remove_files")}
|
||||||
|
descriptionText={t("delete_modal_description", { ns: "downloads" })}
|
||||||
|
onClose={() => {
|
||||||
|
setShowConfirmRemoveFiles(false);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
onConfirm={async () => {
|
||||||
|
setShowConfirmRemoveFiles(false);
|
||||||
|
onClose();
|
||||||
|
await handleRemoveFiles();
|
||||||
|
}}
|
||||||
|
cancelButtonLabel={t("cancel")}
|
||||||
|
confirmButtonLabel={t("remove")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { LibraryGame, ShortcutLocation } from "@types";
|
||||||
|
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
||||||
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
import { logger } from "@renderer/logger";
|
||||||
|
|
||||||
|
export function useGameActions(game: LibraryGame) {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
const { updateLibrary } = useLibrary();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const {
|
||||||
|
removeGameInstaller,
|
||||||
|
removeGameFromLibrary,
|
||||||
|
isGameDeleting,
|
||||||
|
lastPacket,
|
||||||
|
cancelDownload,
|
||||||
|
} = useDownload();
|
||||||
|
|
||||||
|
const [creatingSteamShortcut, setCreatingSteamShortcut] = useState(false);
|
||||||
|
const [isGameRunning, setIsGameRunning] = useState(false);
|
||||||
|
|
||||||
|
const canPlay = Boolean(game.executablePath);
|
||||||
|
const isDeleting = isGameDeleting(game.id);
|
||||||
|
const isGameDownloading =
|
||||||
|
game.download?.status === "active" && lastPacket?.gameId === game.id;
|
||||||
|
const hasRepacks = true;
|
||||||
|
const shouldShowCreateStartMenuShortcut =
|
||||||
|
window.electron.platform === "win32";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onGamesRunning((gamesIds) => {
|
||||||
|
const updatedIsGameRunning =
|
||||||
|
!!game?.id &&
|
||||||
|
!!gamesIds.find((gameRunning) => gameRunning.id == game.id);
|
||||||
|
|
||||||
|
setIsGameRunning(updatedIsGameRunning);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [game?.id]);
|
||||||
|
|
||||||
|
const handlePlayGame = async () => {
|
||||||
|
if (!canPlay) {
|
||||||
|
const path = buildGameDetailsPath({
|
||||||
|
...game,
|
||||||
|
objectId: game.objectId,
|
||||||
|
});
|
||||||
|
if (location.pathname === path) {
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("hydra:openRepacks", {
|
||||||
|
detail: { objectId: game.objectId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate(path, { state: { openRepacks: true } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("hydra:openRepacks", {
|
||||||
|
detail: { objectId: game.objectId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electron.openGame(
|
||||||
|
game.shop,
|
||||||
|
game.objectId,
|
||||||
|
game.executablePath!,
|
||||||
|
game.launchOptions
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast("Failed to start game");
|
||||||
|
logger.error("Failed to start game", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseGame = async () => {
|
||||||
|
try {
|
||||||
|
await window.electron.closeGame(game.shop, game.objectId);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast("Failed to close game");
|
||||||
|
logger.error("Failed to close game", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleFavorite = async () => {
|
||||||
|
try {
|
||||||
|
if (game.favorite) {
|
||||||
|
await window.electron.removeGameFromFavorites(game.shop, game.objectId);
|
||||||
|
showSuccessToast(t("game_removed_from_favorites"));
|
||||||
|
} else {
|
||||||
|
await window.electron.addGameToFavorites(game.shop, game.objectId);
|
||||||
|
showSuccessToast(t("game_added_to_favorites"));
|
||||||
|
}
|
||||||
|
updateLibrary();
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("hydra:game-favorite-toggled", {
|
||||||
|
detail: { shop: game.shop, objectId: game.objectId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(t("failed_update_favorites"));
|
||||||
|
logger.error("Failed to toggle favorite", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateShortcut = async (location: ShortcutLocation) => {
|
||||||
|
try {
|
||||||
|
const success = await window.electron.createGameShortcut(
|
||||||
|
game.shop,
|
||||||
|
game.objectId,
|
||||||
|
location
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showSuccessToast(t("create_shortcut_success"));
|
||||||
|
} else {
|
||||||
|
showErrorToast(t("create_shortcut_error"));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(t("create_shortcut_error"));
|
||||||
|
logger.error("Failed to create shortcut", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateSteamShortcut = async () => {
|
||||||
|
try {
|
||||||
|
setCreatingSteamShortcut(true);
|
||||||
|
await window.electron.createSteamShortcut(game.shop, game.objectId);
|
||||||
|
|
||||||
|
showSuccessToast(
|
||||||
|
t("create_shortcut_success"),
|
||||||
|
t("you_might_need_to_restart_steam")
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to create Steam shortcut", error);
|
||||||
|
showErrorToast(t("create_shortcut_error"));
|
||||||
|
} finally {
|
||||||
|
setCreatingSteamShortcut(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenFolder = async () => {
|
||||||
|
try {
|
||||||
|
await window.electron.openGameExecutablePath(game.shop, game.objectId);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast("Failed to open folder");
|
||||||
|
logger.error("Failed to open folder", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenDownloadOptions = () => {
|
||||||
|
const path = buildGameDetailsPath({
|
||||||
|
...game,
|
||||||
|
objectId: game.objectId,
|
||||||
|
});
|
||||||
|
navigate(path, { state: { openRepacks: true } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("hydra:openRepacks", {
|
||||||
|
detail: { objectId: game.objectId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenGameOptions = () => {
|
||||||
|
const path = buildGameDetailsPath({
|
||||||
|
...game,
|
||||||
|
objectId: game.objectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(path, { state: { openGameOptions: true } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("hydra:openGameOptions", {
|
||||||
|
detail: { objectId: game.objectId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenDownloadLocation = async () => {
|
||||||
|
try {
|
||||||
|
await window.electron.openGameInstallerPath(game.shop, game.objectId);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast("Failed to open download location");
|
||||||
|
logger.error("Failed to open download location", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromLibrary = async () => {
|
||||||
|
try {
|
||||||
|
if (isGameDownloading) {
|
||||||
|
await cancelDownload(game.shop, game.objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await removeGameFromLibrary(game.shop, game.objectId);
|
||||||
|
updateLibrary();
|
||||||
|
showSuccessToast(t("game_removed_from_library"));
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("hydra:game-removed-from-library", {
|
||||||
|
detail: { shop: game.shop, objectId: game.objectId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(t("failed_remove_from_library"));
|
||||||
|
logger.error("Failed to remove from library", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFiles = async () => {
|
||||||
|
try {
|
||||||
|
await removeGameInstaller(game.shop, game.objectId);
|
||||||
|
updateLibrary();
|
||||||
|
showSuccessToast(t("files_removed_success"));
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("hydra:game-files-removed", {
|
||||||
|
detail: { shop: game.shop, objectId: game.objectId },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(t("failed_remove_files"));
|
||||||
|
logger.error("Failed to remove files", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
canPlay,
|
||||||
|
isDeleting,
|
||||||
|
isGameDownloading,
|
||||||
|
isGameRunning,
|
||||||
|
hasRepacks,
|
||||||
|
shouldShowCreateStartMenuShortcut,
|
||||||
|
creatingSteamShortcut,
|
||||||
|
handlePlayGame,
|
||||||
|
handleCloseGame,
|
||||||
|
handleToggleFavorite,
|
||||||
|
handleCreateShortcut,
|
||||||
|
handleCreateSteamShortcut,
|
||||||
|
handleOpenFolder,
|
||||||
|
handleOpenDownloadOptions,
|
||||||
|
handleOpenDownloadLocation,
|
||||||
|
handleRemoveFromLibrary,
|
||||||
|
handleRemoveFiles,
|
||||||
|
handleOpenGameOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,10 +18,18 @@ export function Hero() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
window.electron
|
const language = i18n.language.split("-")[0];
|
||||||
.getTrendingGames()
|
|
||||||
|
window.electron.hydraApi
|
||||||
|
.get<TrendingGame[]>("/catalogue/featured", {
|
||||||
|
params: { language },
|
||||||
|
needsAuth: false,
|
||||||
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
setFeaturedGameDetails(result);
|
setFeaturedGameDetails(result.slice(0, 1));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setFeaturedGameDetails([]);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export * from "./badge/badge";
|
|||||||
export * from "./confirmation-modal/confirmation-modal";
|
export * from "./confirmation-modal/confirmation-modal";
|
||||||
export * from "./suspense-wrapper/suspense-wrapper";
|
export * from "./suspense-wrapper/suspense-wrapper";
|
||||||
export * from "./debrid-badge/debrid-badge";
|
export * from "./debrid-badge/debrid-badge";
|
||||||
|
export * from "./context-menu/context-menu";
|
||||||
|
export * from "./game-context-menu/game-context-menu";
|
||||||
|
export * from "./game-context-menu/use-game-actions";
|
||||||
|
export * from "./star-rating/star-rating";
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import PlayLogo from "@renderer/assets/play-logo.svg?react";
|
|||||||
import { LibraryGame } from "@types";
|
import { LibraryGame } from "@types";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { GameContextMenu } from "..";
|
||||||
|
|
||||||
interface SidebarGameItemProps {
|
interface SidebarGameItemProps {
|
||||||
game: LibraryGame;
|
game: LibraryGame;
|
||||||
@@ -16,6 +18,24 @@ export function SidebarGameItem({
|
|||||||
getGameTitle,
|
getGameTitle,
|
||||||
}: Readonly<SidebarGameItemProps>) {
|
}: Readonly<SidebarGameItemProps>) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
|
visible: boolean;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
}>({ visible: false, position: { x: 0, y: 0 } });
|
||||||
|
|
||||||
|
const handleContextMenu = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
setContextMenu({
|
||||||
|
visible: true,
|
||||||
|
position: { x: event.clientX, y: event.clientY },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseContextMenu = () => {
|
||||||
|
setContextMenu({ visible: false, position: { x: 0, y: 0 } });
|
||||||
|
};
|
||||||
|
|
||||||
const isCustomGame = game.shop === "custom";
|
const isCustomGame = game.shop === "custom";
|
||||||
const sidebarIcon = isCustomGame
|
const sidebarIcon = isCustomGame
|
||||||
@@ -31,34 +51,44 @@ export function SidebarGameItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<>
|
||||||
key={game.id}
|
<li
|
||||||
className={cn("sidebar__menu-item", {
|
key={game.id}
|
||||||
"sidebar__menu-item--active":
|
className={cn("sidebar__menu-item", {
|
||||||
location.pathname === `/game/${game.shop}/${game.objectId}`,
|
"sidebar__menu-item--active":
|
||||||
"sidebar__menu-item--muted": game.download?.status === "removed",
|
location.pathname === `/game/${game.shop}/${game.objectId}`,
|
||||||
})}
|
"sidebar__menu-item--muted": game.download?.status === "removed",
|
||||||
>
|
})}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sidebar__menu-item-button"
|
|
||||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
|
||||||
>
|
>
|
||||||
{sidebarIcon ? (
|
<button
|
||||||
<img
|
type="button"
|
||||||
className="sidebar__game-icon"
|
className="sidebar__menu-item-button"
|
||||||
src={sidebarIcon}
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
alt={game.title}
|
onContextMenu={handleContextMenu}
|
||||||
loading="lazy"
|
>
|
||||||
/>
|
{sidebarIcon ? (
|
||||||
) : (
|
<img
|
||||||
getFallbackIcon()
|
className="sidebar__game-icon"
|
||||||
)}
|
src={sidebarIcon}
|
||||||
|
alt={game.title}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
getFallbackIcon()
|
||||||
|
)}
|
||||||
|
|
||||||
<span className="sidebar__menu-item-button-label">
|
<span className="sidebar__menu-item-button-label">
|
||||||
{getGameTitle(game)}
|
{getGameTitle(game)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<GameContextMenu
|
||||||
|
game={game}
|
||||||
|
visible={contextMenu.visible}
|
||||||
|
position={contextMenu.position}
|
||||||
|
onClose={handleCloseContextMenu}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,23 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--decky {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(22, 177, 149, 0.2) 0%,
|
||||||
|
rgba(62, 98, 192, 0.1) 100%
|
||||||
|
);
|
||||||
|
color: globals.$muted-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(22, 177, 149, 0.3) 0%,
|
||||||
|
rgba(62, 98, 192, 0.15) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__menu-item-button {
|
&__menu-item-button {
|
||||||
@@ -123,6 +140,11 @@
|
|||||||
padding-bottom: globals.$spacing-unit;
|
padding-bottom: globals.$spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__bottom-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
&__help-button {
|
&__help-button {
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Tooltip } from "react-tooltip";
|
|||||||
|
|
||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
|
|
||||||
import { TextField } from "@renderer/components";
|
import { TextField, ConfirmationModal } from "@renderer/components";
|
||||||
import {
|
import {
|
||||||
useDownload,
|
useDownload,
|
||||||
useLibrary,
|
useLibrary,
|
||||||
@@ -31,6 +31,7 @@ import { SidebarGameItem } from "./sidebar-game-item";
|
|||||||
import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal";
|
import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal";
|
||||||
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
import deckyIcon from "@renderer/assets/icons/decky.png";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@@ -47,6 +48,13 @@ export function Sidebar() {
|
|||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
const { library, updateLibrary } = useLibrary();
|
const { library, updateLibrary } = useLibrary();
|
||||||
|
const [deckyPluginInfo, setDeckyPluginInfo] = useState<{
|
||||||
|
installed: boolean;
|
||||||
|
version: string | null;
|
||||||
|
outdated: boolean;
|
||||||
|
}>({ installed: false, version: null, outdated: false });
|
||||||
|
const [homebrewFolderExists, setHomebrewFolderExists] = useState(false);
|
||||||
|
const [showDeckyConfirmModal, setShowDeckyConfirmModal] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
||||||
@@ -66,7 +74,7 @@ export function Sidebar() {
|
|||||||
|
|
||||||
const { lastPacket, progress } = useDownload();
|
const { lastPacket, progress } = useDownload();
|
||||||
|
|
||||||
const { showWarningToast } = useToast();
|
const { showWarningToast, showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
const [showPlayableOnly, setShowPlayableOnly] = useState(false);
|
const [showPlayableOnly, setShowPlayableOnly] = useState(false);
|
||||||
const [showAddGameModal, setShowAddGameModal] = useState(false);
|
const [showAddGameModal, setShowAddGameModal] = useState(false);
|
||||||
@@ -83,10 +91,68 @@ export function Sidebar() {
|
|||||||
setShowAddGameModal(false);
|
setShowAddGameModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadDeckyPluginInfo = async () => {
|
||||||
|
if (window.electron.platform !== "linux") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [info, folderExists] = await Promise.all([
|
||||||
|
window.electron.getHydraDeckyPluginInfo(),
|
||||||
|
window.electron.checkHomebrewFolderExists(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setDeckyPluginInfo({
|
||||||
|
installed: info.installed,
|
||||||
|
version: info.version,
|
||||||
|
outdated: info.outdated,
|
||||||
|
});
|
||||||
|
setHomebrewFolderExists(folderExists);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load Decky plugin info:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstallHydraDeckyPlugin = () => {
|
||||||
|
if (deckyPluginInfo.installed && !deckyPluginInfo.outdated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowDeckyConfirmModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDeckyInstallation = async () => {
|
||||||
|
setShowDeckyConfirmModal(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.installHydraDeckyPlugin();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showSuccessToast(
|
||||||
|
t("decky_plugin_installed", {
|
||||||
|
version: result.currentVersion,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await loadDeckyPluginInfo();
|
||||||
|
} else {
|
||||||
|
showErrorToast(
|
||||||
|
t("decky_plugin_installation_failed", {
|
||||||
|
error: result.error || "Unknown error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(
|
||||||
|
t("decky_plugin_installation_error", { error: String(error) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [lastPacket?.gameId, updateLibrary]);
|
}, [lastPacket?.gameId, updateLibrary]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDeckyPluginInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
|
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
|
||||||
dispatch(setFriendRequestCount(result.friendRequestCount));
|
dispatch(setFriendRequestCount(result.friendRequestCount));
|
||||||
@@ -244,6 +310,30 @@ export function Sidebar() {
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
{window.electron.platform === "linux" && homebrewFolderExists && (
|
||||||
|
<li className="sidebar__menu-item sidebar__menu-item--decky">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sidebar__menu-item-button"
|
||||||
|
onClick={handleInstallHydraDeckyPlugin}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={deckyIcon}
|
||||||
|
alt="Decky"
|
||||||
|
style={{ width: 16, height: 16 }}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{deckyPluginInfo.installed && !deckyPluginInfo.outdated
|
||||||
|
? t("decky_plugin_installed_version", {
|
||||||
|
version: deckyPluginInfo.version,
|
||||||
|
})
|
||||||
|
: deckyPluginInfo.installed && deckyPluginInfo.outdated
|
||||||
|
? t("update_decky_plugin")
|
||||||
|
: t("install_decky_plugin")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -321,18 +411,20 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveSubscription && (
|
<div className="sidebar__bottom-buttons">
|
||||||
<button
|
{hasActiveSubscription && (
|
||||||
type="button"
|
<button
|
||||||
className="sidebar__help-button"
|
type="button"
|
||||||
data-open-support-chat
|
className="sidebar__help-button"
|
||||||
>
|
data-open-support-chat
|
||||||
<div className="sidebar__help-button-icon">
|
>
|
||||||
<CommentDiscussionIcon size={14} />
|
<div className="sidebar__help-button-icon">
|
||||||
</div>
|
<CommentDiscussionIcon size={14} />
|
||||||
<span>{t("need_help")}</span>
|
</div>
|
||||||
</button>
|
<span>{t("need_help")}</span>
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -345,6 +437,24 @@ export function Sidebar() {
|
|||||||
onClose={handleCloseAddGameModal}
|
onClose={handleCloseAddGameModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
visible={showDeckyConfirmModal}
|
||||||
|
title={
|
||||||
|
deckyPluginInfo.installed && deckyPluginInfo.outdated
|
||||||
|
? t("update_decky_plugin_title")
|
||||||
|
: t("install_decky_plugin_title")
|
||||||
|
}
|
||||||
|
descriptionText={
|
||||||
|
deckyPluginInfo.installed && deckyPluginInfo.outdated
|
||||||
|
? t("update_decky_plugin_message")
|
||||||
|
: t("install_decky_plugin_message")
|
||||||
|
}
|
||||||
|
onClose={() => setShowDeckyConfirmModal(false)}
|
||||||
|
onConfirm={handleConfirmDeckyInstallation}
|
||||||
|
cancelButtonLabel={t("cancel")}
|
||||||
|
confirmButtonLabel={t("confirm")}
|
||||||
|
/>
|
||||||
|
|
||||||
<Tooltip id="add-custom-game-tooltip" />
|
<Tooltip id="add-custom-game-tooltip" />
|
||||||
<Tooltip id="show-playable-only-tooltip" />
|
<Tooltip id="show-playable-only-tooltip" />
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
1
src/renderer/src/components/star-rating/index.ts
Normal file
1
src/renderer/src/components/star-rating/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./star-rating";
|
||||||
54
src/renderer/src/components/star-rating/star-rating.scss
Normal file
54
src/renderer/src/components/star-rating/star-rating.scss
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.star-rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
&__star {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
transition: color ease 0.2s;
|
||||||
|
|
||||||
|
&--filled {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--empty {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--half {
|
||||||
|
color: #ffffff;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
clip-path: polygon(0 0, 50% 0, 50% 100%, 0 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__half-star {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__calculating-text,
|
||||||
|
&__no-rating-text {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--calculating,
|
||||||
|
&--no-rating {
|
||||||
|
.star-rating__star {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/renderer/src/components/star-rating/star-rating.tsx
Normal file
77
src/renderer/src/components/star-rating/star-rating.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { StarIcon, StarFillIcon } from "@primer/octicons-react";
|
||||||
|
import "./star-rating.scss";
|
||||||
|
|
||||||
|
export interface StarRatingProps {
|
||||||
|
rating: number | null;
|
||||||
|
maxStars?: number;
|
||||||
|
size?: number;
|
||||||
|
showCalculating?: boolean;
|
||||||
|
calculatingText?: string;
|
||||||
|
hideIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StarRating({
|
||||||
|
rating,
|
||||||
|
maxStars = 5,
|
||||||
|
size = 12,
|
||||||
|
showCalculating = false,
|
||||||
|
calculatingText = "Calculating",
|
||||||
|
hideIcon = false,
|
||||||
|
}: Readonly<StarRatingProps>) {
|
||||||
|
if (rating === null && showCalculating) {
|
||||||
|
return (
|
||||||
|
<div className="star-rating star-rating--calculating">
|
||||||
|
{!hideIcon && <StarIcon size={size} />}
|
||||||
|
<span className="star-rating__calculating-text">{calculatingText}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rating === null || rating === undefined) {
|
||||||
|
return (
|
||||||
|
<div className="star-rating star-rating--no-rating">
|
||||||
|
{!hideIcon && <StarIcon size={size} />}
|
||||||
|
<span className="star-rating__no-rating-text">…</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filledStars = Math.floor(rating);
|
||||||
|
const hasHalfStar = rating % 1 >= 0.5;
|
||||||
|
const emptyStars = maxStars - filledStars - (hasHalfStar ? 1 : 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="star-rating">
|
||||||
|
{Array.from({ length: filledStars }, (_, index) => (
|
||||||
|
<StarFillIcon
|
||||||
|
key={`filled-${index}`}
|
||||||
|
size={size}
|
||||||
|
className="star-rating__star star-rating__star--filled"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{hasHalfStar && (
|
||||||
|
<div className="star-rating__half-star" key="half-star">
|
||||||
|
<StarIcon
|
||||||
|
size={size}
|
||||||
|
className="star-rating__star star-rating__star--empty"
|
||||||
|
/>
|
||||||
|
<StarFillIcon
|
||||||
|
size={size}
|
||||||
|
className="star-rating__star star-rating__star--half"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Array.from({ length: emptyStars }, (_, index) => (
|
||||||
|
<StarIcon
|
||||||
|
key={`empty-${index}`}
|
||||||
|
size={size}
|
||||||
|
className="star-rating__star star-rating__star--empty"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<span className="star-rating__value">{rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
|
|
||||||
export const VERSION_CODENAME = "Lumen";
|
export const VERSION_CODENAME = "Supernova";
|
||||||
|
|
||||||
export const DOWNLOADER_NAME = {
|
export const DOWNLOADER_NAME = {
|
||||||
[Downloader.RealDebrid]: "Real-Debrid",
|
[Downloader.RealDebrid]: "Real-Debrid",
|
||||||
@@ -11,6 +11,7 @@ export const DOWNLOADER_NAME = {
|
|||||||
[Downloader.Datanodes]: "Datanodes",
|
[Downloader.Datanodes]: "Datanodes",
|
||||||
[Downloader.Mediafire]: "Mediafire",
|
[Downloader.Mediafire]: "Mediafire",
|
||||||
[Downloader.TorBox]: "TorBox",
|
[Downloader.TorBox]: "TorBox",
|
||||||
|
[Downloader.AllDebrid]: "All-Debrid",
|
||||||
[Downloader.Hydra]: "Nimbus",
|
[Downloader.Hydra]: "Nimbus",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function CloudSyncContextProvider({
|
|||||||
const [loadingPreview, setLoadingPreview] = useState(false);
|
const [loadingPreview, setLoadingPreview] = useState(false);
|
||||||
const [freezingArtifact, setFreezingArtifact] = useState(false);
|
const [freezingArtifact, setFreezingArtifact] = useState(false);
|
||||||
|
|
||||||
const { showSuccessToast } = useToast();
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
const downloadGameArtifact = useCallback(
|
const downloadGameArtifact = useCallback(
|
||||||
async (gameArtifactId: string) => {
|
async (gameArtifactId: string) => {
|
||||||
@@ -98,7 +98,18 @@ export function CloudSyncContextProvider({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getGameArtifacts = useCallback(async () => {
|
const getGameArtifacts = useCallback(async () => {
|
||||||
const results = await window.electron.getGameArtifacts(objectId, shop);
|
const params = new URLSearchParams({
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await window.electron.hydraApi
|
||||||
|
.get<GameArtifact[]>(`/profile/games/artifacts?${params.toString()}`, {
|
||||||
|
needsSubscription: true,
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
setArtifacts(results);
|
setArtifacts(results);
|
||||||
}, [objectId, shop]);
|
}, [objectId, shop]);
|
||||||
|
|
||||||
@@ -122,16 +133,25 @@ export function CloudSyncContextProvider({
|
|||||||
const uploadSaveGame = useCallback(
|
const uploadSaveGame = useCallback(
|
||||||
async (downloadOptionTitle: string | null) => {
|
async (downloadOptionTitle: string | null) => {
|
||||||
setUploadingBackup(true);
|
setUploadingBackup(true);
|
||||||
window.electron.uploadSaveGame(objectId, shop, downloadOptionTitle);
|
window.electron
|
||||||
|
.uploadSaveGame(objectId, shop, downloadOptionTitle)
|
||||||
|
.catch((err) => {
|
||||||
|
setUploadingBackup(false);
|
||||||
|
logger.error("Failed to upload save game", { objectId, shop, err });
|
||||||
|
showErrorToast(t("backup_failed"));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[objectId, shop]
|
[objectId, shop, t, showErrorToast]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleArtifactFreeze = useCallback(
|
const toggleArtifactFreeze = useCallback(
|
||||||
async (gameArtifactId: string, freeze: boolean) => {
|
async (gameArtifactId: string, freeze: boolean) => {
|
||||||
setFreezingArtifact(true);
|
setFreezingArtifact(true);
|
||||||
try {
|
try {
|
||||||
await window.electron.toggleArtifactFreeze(gameArtifactId, freeze);
|
const endpoint = freeze ? "freeze" : "unfreeze";
|
||||||
|
await window.electron.hydraApi.put(
|
||||||
|
`/profile/games/artifacts/${gameArtifactId}/${endpoint}`
|
||||||
|
);
|
||||||
getGameArtifacts();
|
getGameArtifacts();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Failed to toggle artifact freeze", objectId, shop, err);
|
logger.error("Failed to toggle artifact freeze", objectId, shop, err);
|
||||||
@@ -179,10 +199,12 @@ export function CloudSyncContextProvider({
|
|||||||
|
|
||||||
const deleteGameArtifact = useCallback(
|
const deleteGameArtifact = useCallback(
|
||||||
async (gameArtifactId: string) => {
|
async (gameArtifactId: string) => {
|
||||||
return window.electron.deleteGameArtifact(gameArtifactId).then(() => {
|
return window.electron.hydraApi
|
||||||
getGameBackupPreview();
|
.delete<{ ok: boolean }>(`/profile/games/artifacts/${gameArtifactId}`)
|
||||||
getGameArtifacts();
|
.then(() => {
|
||||||
});
|
getGameBackupPreview();
|
||||||
|
getGameArtifacts();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[getGameBackupPreview, getGameArtifacts]
|
[getGameBackupPreview, getGameArtifacts]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
import { GameDetailsContext } from "./game-details.context.types";
|
import { GameDetailsContext } from "./game-details.context.types";
|
||||||
import { SteamContentDescriptor } from "@shared";
|
import { SteamContentDescriptor } from "@shared";
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ export function GameDetailsContextProvider({
|
|||||||
}, [getRepacksForObjectId, objectId]);
|
}, [getRepacksForObjectId, objectId]);
|
||||||
|
|
||||||
const { i18n } = useTranslation("game_details");
|
const { i18n } = useTranslation("game_details");
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -140,29 +142,24 @@ export function GameDetailsContextProvider({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const statsPromise = window.electron
|
window.electron.getGameStats(objectId, shop).then((result) => {
|
||||||
.getGameStats(objectId, shop)
|
if (abortController.signal.aborted) return;
|
||||||
.then((result) => {
|
setStats(result);
|
||||||
if (abortController.signal.aborted) return null;
|
});
|
||||||
setStats(result);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([shopDetailsPromise, statsPromise])
|
const assetsPromise = window.electron.getGameAssets(objectId, shop);
|
||||||
.then(([_, stats]) => {
|
|
||||||
if (stats) {
|
|
||||||
const assets = stats.assets;
|
|
||||||
if (assets) {
|
|
||||||
window.electron.saveGameShopAssets(objectId, shop, assets);
|
|
||||||
|
|
||||||
setShopDetails((prev) => {
|
Promise.all([shopDetailsPromise, assetsPromise])
|
||||||
if (!prev) return null;
|
.then(([_, assets]) => {
|
||||||
return {
|
if (assets) {
|
||||||
...prev,
|
if (abortController.signal.aborted) return;
|
||||||
assets,
|
setShopDetails((prev) => {
|
||||||
};
|
if (!prev) return null;
|
||||||
});
|
return {
|
||||||
}
|
...prev,
|
||||||
|
assets,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -177,7 +174,7 @@ export function GameDetailsContextProvider({
|
|||||||
if (abortController.signal.aborted) return;
|
if (abortController.signal.aborted) return;
|
||||||
setAchievements(achievements);
|
setAchievements(achievements);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => void 0);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
updateGame,
|
updateGame,
|
||||||
@@ -198,6 +195,19 @@ export function GameDetailsContextProvider({
|
|||||||
dispatch(setHeaderTitle(gameTitle));
|
dispatch(setHeaderTitle(gameTitle));
|
||||||
}, [objectId, gameTitle, dispatch]);
|
}, [objectId, gameTitle, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const state =
|
||||||
|
(location && (location.state as Record<string, unknown>)) || {};
|
||||||
|
if (state.openRepacks) {
|
||||||
|
setShowRepacksModal(true);
|
||||||
|
try {
|
||||||
|
window.history.replaceState({}, document.title, location.pathname);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (game?.title) {
|
if (game?.title) {
|
||||||
dispatch(setHeaderTitle(game.title));
|
dispatch(setHeaderTitle(game.title));
|
||||||
@@ -222,6 +232,61 @@ export function GameDetailsContextProvider({
|
|||||||
};
|
};
|
||||||
}, [game?.id, isGameRunning, updateGame]);
|
}, [game?.id, isGameRunning, updateGame]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (ev: Event) => {
|
||||||
|
try {
|
||||||
|
const detail = (ev as CustomEvent).detail || {};
|
||||||
|
if (detail.objectId && detail.objectId === objectId) {
|
||||||
|
setShowRepacksModal(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("hydra:openRepacks", handler as EventListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("hydra:openRepacks", handler as EventListener);
|
||||||
|
};
|
||||||
|
}, [objectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (ev: Event) => {
|
||||||
|
try {
|
||||||
|
const detail = (ev as CustomEvent).detail || {};
|
||||||
|
if (detail.objectId && detail.objectId === objectId) {
|
||||||
|
setShowGameOptionsModal(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
void e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("hydra:openGameOptions", handler as EventListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
"hydra:openGameOptions",
|
||||||
|
handler as EventListener
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [objectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const state =
|
||||||
|
(location && (location.state as Record<string, unknown>)) || {};
|
||||||
|
if (state.openGameOptions) {
|
||||||
|
setShowGameOptionsModal(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.history.replaceState({}, document.title, location.pathname);
|
||||||
|
} catch (_e) {
|
||||||
|
void _e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
const lastDownloadedOption = useMemo(() => {
|
const lastDownloadedOption = useMemo(() => {
|
||||||
if (game?.download) {
|
if (game?.download) {
|
||||||
const repack = repacks.find((repack) =>
|
const repack = repacks.find((repack) =>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export function SettingsContextProvider({
|
|||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const defaultSourceUrl = searchParams.get("urls");
|
const defaultSourceUrl = searchParams.get("urls");
|
||||||
|
const defaultTab = searchParams.get("tab");
|
||||||
const defaultAppearanceTheme = searchParams.get("theme");
|
const defaultAppearanceTheme = searchParams.get("theme");
|
||||||
const defaultAppearanceAuthorId = searchParams.get("authorId");
|
const defaultAppearanceAuthorId = searchParams.get("authorId");
|
||||||
const defaultAppearanceAuthorName = searchParams.get("authorName");
|
const defaultAppearanceAuthorName = searchParams.get("authorName");
|
||||||
@@ -77,6 +78,13 @@ export function SettingsContextProvider({
|
|||||||
}
|
}
|
||||||
}, [defaultSourceUrl]);
|
}, [defaultSourceUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultTab) {
|
||||||
|
const idx = Number(defaultTab);
|
||||||
|
if (!Number.isNaN(idx)) setCurrentCategoryIndex(idx);
|
||||||
|
}
|
||||||
|
}, [defaultTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appearance.theme) setCurrentCategoryIndex(3);
|
if (appearance.theme) setCurrentCategoryIndex(3);
|
||||||
}, [appearance.theme]);
|
}, [appearance.theme]);
|
||||||
@@ -108,7 +116,13 @@ export function SettingsContextProvider({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchBlockedUsers = useCallback(async () => {
|
const fetchBlockedUsers = useCallback(async () => {
|
||||||
const blockedUsers = await window.electron.getBlockedUsers(12, 0);
|
const blockedUsers = await window.electron.hydraApi
|
||||||
|
.get<UserBlocks>("/profile/blocks", {
|
||||||
|
params: { take: 12, skip: 0 },
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return { blocks: [] };
|
||||||
|
});
|
||||||
setBlockedUsers(blockedUsers.blocks);
|
setBlockedUsers(blockedUsers.blocks);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -82,26 +82,38 @@ export function UserProfileContextProvider({
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const { t } = useTranslation("user_profile");
|
const { t, i18n } = useTranslation("user_profile");
|
||||||
|
|
||||||
const { showErrorToast } = useToast();
|
const { showErrorToast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const getUserStats = useCallback(async () => {
|
const getUserStats = useCallback(async () => {
|
||||||
window.electron.getUserStats(userId).then((stats) => {
|
window.electron.hydraApi
|
||||||
setUserStats(stats);
|
.get<UserStats>(`/users/${userId}/stats`)
|
||||||
});
|
.then((stats) => {
|
||||||
|
setUserStats(stats);
|
||||||
|
});
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
const getUserLibraryGames = useCallback(
|
const getUserLibraryGames = useCallback(
|
||||||
async (sortBy?: string) => {
|
async (sortBy?: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await window.electron.getUserLibrary(
|
const params = new URLSearchParams();
|
||||||
userId,
|
params.append("take", "12");
|
||||||
12,
|
params.append("skip", "0");
|
||||||
0,
|
if (sortBy) {
|
||||||
sortBy
|
params.append("sortBy", sortBy);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
const url = queryString
|
||||||
|
? `/users/${userId}/library?${queryString}`
|
||||||
|
: `/users/${userId}/library`;
|
||||||
|
|
||||||
|
const response = await window.electron.hydraApi.get<{
|
||||||
|
library: UserGame[];
|
||||||
|
pinnedGames: UserGame[];
|
||||||
|
}>(url);
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
setLibraryGames(response.library);
|
setLibraryGames(response.library);
|
||||||
@@ -122,26 +134,36 @@ export function UserProfileContextProvider({
|
|||||||
getUserStats();
|
getUserStats();
|
||||||
getUserLibraryGames();
|
getUserLibraryGames();
|
||||||
|
|
||||||
return window.electron.getUser(userId).then((userProfile) => {
|
return window.electron.hydraApi
|
||||||
if (userProfile) {
|
.get<UserProfile | null>(`/users/${userId}`)
|
||||||
setUserProfile(userProfile);
|
.then((userProfile) => {
|
||||||
|
if (userProfile) {
|
||||||
|
setUserProfile(userProfile);
|
||||||
|
|
||||||
if (userProfile.profileImageUrl) {
|
if (userProfile.profileImageUrl) {
|
||||||
getHeroBackgroundFromImageUrl(userProfile.profileImageUrl).then(
|
getHeroBackgroundFromImageUrl(userProfile.profileImageUrl).then(
|
||||||
(color) => setHeroBackground(color)
|
(color) => setHeroBackground(color)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showErrorToast(t("user_not_found"));
|
||||||
|
navigate(-1);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
showErrorToast(t("user_not_found"));
|
|
||||||
navigate(-1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [navigate, getUserStats, getUserLibraryGames, showErrorToast, userId, t]);
|
}, [navigate, getUserStats, getUserLibraryGames, showErrorToast, userId, t]);
|
||||||
|
|
||||||
const getBadges = useCallback(async () => {
|
const getBadges = useCallback(async () => {
|
||||||
const badges = await window.electron.getBadges();
|
const language = i18n.language.split("-")[0];
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
locale: language,
|
||||||
|
});
|
||||||
|
|
||||||
|
const badges = await window.electron.hydraApi.get<Badge[]>(
|
||||||
|
`/badges?${params.toString()}`,
|
||||||
|
{ needsAuth: false }
|
||||||
|
);
|
||||||
setBadges(badges);
|
setBadges(badges);
|
||||||
}, []);
|
}, [i18n]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUserProfile(null);
|
setUserProfile(null);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user