mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-12 06:16:17 +00:00
Compare commits
155 Commits
v3.2.2
...
fix/fixing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4043c2c4 | ||
|
|
97cf02577a | ||
|
|
fbce53d61a | ||
|
|
1835adf8b4 | ||
|
|
7fd0894b56 | ||
|
|
3bb8c17b16 | ||
|
|
417bb69b86 | ||
|
|
a55b5dafa0 | ||
|
|
b7bec47a2c | ||
|
|
f565226a6d | ||
|
|
e7427b8184 | ||
|
|
cc505acbdc | ||
|
|
934831eaae | ||
|
|
a35dae3e18 | ||
|
|
5e58762151 | ||
|
|
21535d8acc | ||
|
|
6106325f0a | ||
|
|
3ff9dd705e | ||
|
|
04f061afa3 | ||
|
|
f8ea4f29d9 | ||
|
|
1520e2b2ae | ||
|
|
d29fc50f68 | ||
|
|
ede538392f | ||
|
|
f464850c38 | ||
|
|
379e90568a | ||
|
|
01d440590b | ||
|
|
56da86d63d | ||
|
|
36cf2060c1 | ||
|
|
5fa4d128c3 | ||
|
|
d15ef33a86 | ||
|
|
54ab719935 | ||
|
|
9eb909cd17 | ||
|
|
b2b4b1ff3c | ||
|
|
f547b85df7 | ||
|
|
0d75878b07 | ||
|
|
9470068de6 | ||
|
|
74a92f57bd | ||
|
|
73f4b0e869 | ||
|
|
e6fde48dbd | ||
|
|
9b8a0af8e9 | ||
|
|
31319b0811 | ||
|
|
9976812bf7 | ||
|
|
6b4d422b65 | ||
|
|
ec9db3a48f | ||
|
|
72cde368f5 | ||
|
|
bd77aee3c2 | ||
|
|
dafdda1bc7 | ||
|
|
6c6a238c5f | ||
|
|
0df18022ed | ||
|
|
6626368613 | ||
|
|
cff728bbbe | ||
|
|
3961b53d0e | ||
|
|
3f08e87eda | ||
|
|
b84c34a69c | ||
|
|
c2e1a092c9 | ||
|
|
fe03894054 | ||
|
|
893695eda0 | ||
|
|
28727600fe | ||
|
|
d892ed4f34 | ||
|
|
ea618febe2 | ||
|
|
358eac061f | ||
|
|
33f9e5729b | ||
|
|
2a7feba5dc | ||
|
|
1390f99895 | ||
|
|
daf8ee6ba2 | ||
|
|
652fe69141 | ||
|
|
61b2710219 | ||
|
|
0e67e21223 | ||
|
|
f12784b6e7 | ||
|
|
8795359c0b | ||
|
|
bcc2127a6e | ||
|
|
766c5760d7 | ||
|
|
3c8db34456 | ||
|
|
433ebf55ac | ||
|
|
b68bed97dd | ||
|
|
2a8600a841 | ||
|
|
3fd48eb784 | ||
|
|
aa19241b2b | ||
|
|
a16a75ff39 | ||
|
|
e4d97e9775 | ||
|
|
5688aaf0e8 | ||
|
|
ad5eb22553 | ||
|
|
1c3a5f4800 | ||
|
|
d8a7275512 | ||
|
|
489dd710fa | ||
|
|
0db93c7dad | ||
|
|
d9fe3c709e | ||
|
|
03a3d734f1 | ||
|
|
106d66f1a2 | ||
|
|
3ef6472825 | ||
|
|
df77d754ae | ||
|
|
6a8da74bcf | ||
|
|
1aa3bb1ad2 | ||
|
|
fdcf244f4e | ||
|
|
d8a4eaaa66 | ||
|
|
1c71c20b48 | ||
|
|
aefc5eca49 | ||
|
|
5f1b83a0d9 | ||
|
|
85efc23c25 | ||
|
|
b344a1850a | ||
|
|
497f5e7742 | ||
|
|
199b0d5b19 | ||
|
|
2554dc4c69 | ||
|
|
c4c401e054 | ||
|
|
864ff0070f | ||
|
|
da8c40d5dc | ||
|
|
a32fdf3385 | ||
|
|
c9c1750afb | ||
|
|
857063d2c7 | ||
|
|
ed699a8dee | ||
|
|
a9ec1a62bf | ||
|
|
778e921594 | ||
|
|
206886c091 | ||
|
|
af2896efc3 | ||
|
|
c7735362e0 | ||
|
|
886e176b08 | ||
|
|
f522a7c9ef | ||
|
|
69f4ce821f | ||
|
|
8bfd6e5547 | ||
|
|
1cbde684e7 | ||
|
|
e5f7e9addc | ||
|
|
6832c11d06 | ||
|
|
740cc2ec7c | ||
|
|
e12c6daa16 | ||
|
|
2ee8a64945 | ||
|
|
a54edd8b4a | ||
|
|
f7b6a91621 | ||
|
|
96dbfe8593 | ||
|
|
8513f83169 | ||
|
|
b116e29dc0 | ||
|
|
52b291fb24 | ||
|
|
dddc5bfd96 | ||
|
|
282bb45869 | ||
|
|
c982ebe33a | ||
|
|
08935ceabd | ||
|
|
0e7a0d1afa | ||
|
|
38dc8bdea5 | ||
|
|
c33aa3c874 | ||
|
|
afa34c74d7 | ||
|
|
cda9b8fb42 | ||
|
|
026abd7909 | ||
|
|
bc31a28eee | ||
|
|
f1624441b4 | ||
|
|
358abde963 | ||
|
|
c65fc15d99 | ||
|
|
b32226273e | ||
|
|
2e7a2a05a5 | ||
|
|
ab70ff10be | ||
|
|
6a429f9f39 | ||
|
|
b03c69b185 | ||
|
|
3fe77f4961 | ||
|
|
79be151de0 | ||
|
|
e804b92f30 | ||
|
|
cf31b36961 | ||
|
|
3156baf78e |
@@ -1,2 +1,4 @@
|
||||
MAIN_VITE_API_URL=API_URL
|
||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE=
|
||||
|
||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -47,12 +47,13 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -61,14 +62,15 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Test Upload build
|
||||
- name: Upload build
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||
@@ -78,7 +80,6 @@ jobs:
|
||||
BUILDS_URL: ${{ secrets.BUILDS_URL }}
|
||||
BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
|
||||
run: node scripts/upload-build.cjs
|
||||
|
||||
- name: Create artifact
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -49,13 +49,13 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -64,13 +64,13 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Create artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
13
.github/workflows/trigger-lp.yml
vendored
Normal file
13
.github/workflows/trigger-lp.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Trigger Landing Page Build
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Trigger Landing Page build
|
||||
run: curl --location --request POST '${{ secrets.LP_TRIGGER_DEPLOY_URL }}'
|
||||
BIN
binaries/7z.dll
Normal file
BIN
binaries/7z.dll
Normal file
Binary file not shown.
BIN
binaries/7z.exe
Normal file
BIN
binaries/7z.exe
Normal file
Binary file not shown.
BIN
binaries/7zz
Normal file
BIN
binaries/7zz
Normal file
Binary file not shown.
BIN
binaries/7zzs
Normal file
BIN
binaries/7zzs
Normal file
Binary file not shown.
@@ -20,6 +20,9 @@ asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
executableName: Hydra
|
||||
extraResources:
|
||||
- from: binaries/7z.exe
|
||||
- from: binaries/7z.dll
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
@@ -35,6 +38,8 @@ portable:
|
||||
artifactName: ${name}-${version}-portable.${ext}
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extraResources:
|
||||
- from: binaries/7zz
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
@@ -44,6 +49,8 @@ mac:
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
extraResources:
|
||||
- from: binaries/7zzs
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
|
||||
@@ -16,9 +16,6 @@ export default defineConfig(({ mode }) => {
|
||||
main: {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: ["better-sqlite3"],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.2.2",
|
||||
"version": "3.4.0",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -28,8 +28,7 @@
|
||||
"build:win": "electron-vite build && electron-builder --win",
|
||||
"build:mac": "electron-vite build && electron-builder --mac",
|
||||
"build:linux": "electron-vite build && electron-builder --linux",
|
||||
"prepare": "husky",
|
||||
"knex:migrate:make": "knex --knexfile src/main/knexfile.ts migrate:make --esm"
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
@@ -44,7 +43,7 @@
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"axios-cookiejar-support": "^5.0.5",
|
||||
"classic-level": "^2.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
@@ -61,7 +60,6 @@
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kill-port": "^2.0.1",
|
||||
"knex": "^3.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
"piscina": "^4.7.0",
|
||||
@@ -71,9 +69,11 @@
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-redux": "^9.1.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"sound-play": "^1.1.0",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tar": "^7.4.3",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"user-agents": "^1.1.387",
|
||||
"yaml": "^2.6.1",
|
||||
"yup": "^1.5.0",
|
||||
|
||||
@@ -1,48 +1,61 @@
|
||||
import aria2p
|
||||
|
||||
class HttpDownloader:
|
||||
def __init__(self):
|
||||
self.download = None
|
||||
self.aria2 = aria2p.API(
|
||||
aria2p.Client(
|
||||
host="http://localhost",
|
||||
port=6800,
|
||||
secret=""
|
||||
)
|
||||
)
|
||||
def __init__(self):
|
||||
self.download = None
|
||||
self.aria2 = aria2p.API(
|
||||
aria2p.Client(
|
||||
host="http://localhost",
|
||||
port=6800,
|
||||
secret=""
|
||||
)
|
||||
)
|
||||
|
||||
def start_download(self, url: str, save_path: str, header: str, out: str = None):
|
||||
if self.download:
|
||||
self.aria2.resume([self.download])
|
||||
else:
|
||||
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
|
||||
|
||||
self.download = downloads[0]
|
||||
|
||||
def pause_download(self):
|
||||
if self.download:
|
||||
self.aria2.pause([self.download])
|
||||
|
||||
def cancel_download(self):
|
||||
if self.download:
|
||||
self.aria2.remove([self.download])
|
||||
self.download = None
|
||||
def start_download(self, url: str, save_path: str, header: str, out: str = None, allow_multiple_connections: bool = False):
|
||||
if self.download:
|
||||
self.aria2.resume([self.download])
|
||||
else:
|
||||
options = {
|
||||
"header": header,
|
||||
"dir": save_path,
|
||||
"out": out
|
||||
}
|
||||
|
||||
def get_download_status(self):
|
||||
if self.download == None:
|
||||
return None
|
||||
if allow_multiple_connections:
|
||||
options.update({
|
||||
"split": "16",
|
||||
"max-connection-per-server": "16",
|
||||
"min-split-size": "1M"
|
||||
})
|
||||
|
||||
downloads = self.aria2.add(url, options=options)
|
||||
|
||||
self.download = downloads[0]
|
||||
|
||||
def pause_download(self):
|
||||
if self.download:
|
||||
self.aria2.pause([self.download])
|
||||
|
||||
def cancel_download(self):
|
||||
if self.download:
|
||||
self.aria2.remove([self.download])
|
||||
self.download = None
|
||||
|
||||
download = self.aria2.get_download(self.download.gid)
|
||||
def get_download_status(self):
|
||||
if self.download == None:
|
||||
return None
|
||||
|
||||
response = {
|
||||
'folderName': download.name,
|
||||
'fileSize': download.total_length,
|
||||
'progress': download.completed_length / download.total_length if download.total_length else 0,
|
||||
'downloadSpeed': download.download_speed,
|
||||
'numPeers': 0,
|
||||
'numSeeds': 0,
|
||||
'status': download.status,
|
||||
'bytesDownloaded': download.completed_length,
|
||||
}
|
||||
download = self.aria2.get_download(self.download.gid)
|
||||
|
||||
return response
|
||||
response = {
|
||||
'folderName': download.name,
|
||||
'fileSize': download.total_length,
|
||||
'progress': download.completed_length / download.total_length if download.total_length else 0,
|
||||
'downloadSpeed': download.download_speed,
|
||||
'numPeers': 0,
|
||||
'numSeeds': 0,
|
||||
'status': download.status,
|
||||
'bytesDownloaded': download.completed_length,
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
@@ -147,11 +147,11 @@ def action():
|
||||
torrent_downloader.start_download(url, data['save_path'])
|
||||
else:
|
||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'), data.get('allow_multiple_connections', False))
|
||||
else:
|
||||
http_downloader = HttpDownloader()
|
||||
downloads[game_id] = http_downloader
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'), data.get('allow_multiple_connections', False))
|
||||
|
||||
downloading_game_id = game_id
|
||||
|
||||
|
||||
@@ -123,3 +123,14 @@ const copyAria2 = () => {
|
||||
|
||||
copyAria2();
|
||||
downloadLudusavi();
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
const binariesPath = path.join(__dirname, "..", "binaries");
|
||||
|
||||
if (fs.existsSync(binariesPath)) {
|
||||
const zzzPath = path.join(binariesPath, "7zz");
|
||||
const zzzsPath = path.join(binariesPath, "7zzs");
|
||||
if (fs.existsSync(zzzPath)) fs.chmodSync(zzzPath, 0o755);
|
||||
if (fs.existsSync(zzzsPath)) fs.chmodSync(zzzsPath, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -255,6 +255,12 @@
|
||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL للمصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||
"validate_download_source": "تحقق",
|
||||
"remove_download_source": "إزالة",
|
||||
"removed_download_sources": "تمت إزالة مصادر التنزيل",
|
||||
"cancel_button_confirmation_delete_all_sources": "لا",
|
||||
"confirm_button_confirmation_delete_all_sources": "نعم، احذف كل شيء",
|
||||
"description_confirmation_delete_all_sources": "سوف تقوم بحذف جميع مصادر التنزيل",
|
||||
"title_confirmation_delete_all_sources": "احذف جميع مصادر التنزيل",
|
||||
"button_delete_all_sources": "قم بإزالة جميع مصادر التنزيل",
|
||||
"add_download_source": "إضافة مصدر",
|
||||
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"notifications": "Апавяшчэнні",
|
||||
"enable_download_notifications": "Па сканчэнні сцягванні",
|
||||
"enable_repack_list_notifications": "Пры даданні новага рэпака",
|
||||
"cancel_button_confirmation_delete_all_sources": "Няма",
|
||||
"confirm_button_confirmation_delete_all_sources": "Так,выдаліць усё",
|
||||
"description_confirmation_delete_all_sources": "Вы выдаліце ўсе крыніцы сцягвання",
|
||||
"title_confirmation_delete_all_sources": "Выдаліць усе крыніцы сцягвання",
|
||||
"removed_download_sources": "Крыніцы сцягвання выдалены",
|
||||
"button_delete_all_sources": "Премахнете всички източници на изтегляне",
|
||||
"behavior": "Паводзіны",
|
||||
"quit_app_instead_hiding": "Закрываць праграму замест таго, каб хаваць яе ў трэй",
|
||||
"launch_with_system": "Запускаць праграму пры запуску сыстэмы"
|
||||
|
||||
@@ -253,6 +253,12 @@
|
||||
"download_source_errored": "Сгрешен",
|
||||
"sync_download_sources": "Синхронизирай източниците",
|
||||
"removed_download_source": "Източника за сваляне е премахнат",
|
||||
"cancel_button_confirmation_delete_all_sources": "не",
|
||||
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
||||
"description_confirmation_delete_all_sources": "Вы удалите все источники загрузки",
|
||||
"title_confirmation_delete_all_sources": "Удалить все источники загрузки",
|
||||
"removed_download_sources": "Шрифты удалены",
|
||||
"button_delete_all_sources": "Удалить все источники загрузки",
|
||||
"added_download_source": "Добавен източник за сваляне",
|
||||
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
|
||||
"insert_valid_json_url": "Добавете ваиден JSON линк",
|
||||
|
||||
@@ -175,6 +175,12 @@
|
||||
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
||||
"validate_download_source": "Valida",
|
||||
"remove_download_source": "Elimina",
|
||||
"cancel_button_confirmation_delete_all_sources": "No",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sí, esborra-ho tot",
|
||||
"description_confirmation_delete_all_sources": "Eliminareu totes les fonts de descàrrega",
|
||||
"title_confirmation_delete_all_sources": "Suprimeix totes les fonts de baixada",
|
||||
"removed_download_sources": "S'han eliminat les fonts de descàrrega",
|
||||
"button_delete_all_sources": "Elimina totes les fonts de baixada",
|
||||
"add_download_source": "Afegeix font",
|
||||
"download_count_zero": "No hi ha baixades a la llista",
|
||||
"download_count_one": "{{countFormatted}} a la llista de baixades",
|
||||
|
||||
@@ -237,6 +237,12 @@
|
||||
"download_source_errored": "Chyba",
|
||||
"sync_download_sources": "Synchronizovat zdroje",
|
||||
"removed_download_source": "Zdroj odebrán",
|
||||
"cancel_button_confirmation_delete_all_sources": "Žádný",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ano, smazat vše",
|
||||
"description_confirmation_delete_all_sources": "Smažete všechny zdroje stahování",
|
||||
"title_confirmation_delete_all_sources": "Odstraňte všechny zdroje stahování",
|
||||
"removed_download_sources": "Zdroje stahování byly odstraněny",
|
||||
"button_delete_all_sources": "Odstraňte všechny zdroje stahování",
|
||||
"added_download_source": "Zdroj přidán",
|
||||
"download_sources_synced": "Všechny zdroje jsou synchronizovány",
|
||||
"insert_valid_json_url": "Zadej platnou JSON adresu",
|
||||
|
||||
@@ -201,6 +201,12 @@
|
||||
"download_source_errored": "Fejlede",
|
||||
"sync_download_sources": "Synkronisér kilder",
|
||||
"removed_download_source": "Download kilde fjernet",
|
||||
"cancel_button_confirmation_delete_all_sources": "Ingen",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, slet alt",
|
||||
"description_confirmation_delete_all_sources": "Du vil slette alle downloadkilder",
|
||||
"title_confirmation_delete_all_sources": "Slet alle downloadkilder",
|
||||
"removed_download_sources": "Download kilder fjernet",
|
||||
"button_delete_all_sources": "Fjern alle downloadkilder",
|
||||
"added_download_source": "Tilføjede download kilde",
|
||||
"download_sources_synced": "Alle download kilder er synkroniserede",
|
||||
"insert_valid_json_url": "Indsæt en gyldig JSON url",
|
||||
|
||||
@@ -185,6 +185,12 @@
|
||||
"download_source_errored": "Fehlgeschlagen",
|
||||
"sync_download_sources": "Quellen synchronisieren",
|
||||
"removed_download_source": "Download-Quelle entfernt",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nein",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, alles löschen",
|
||||
"description_confirmation_delete_all_sources": "Du löschen alle Downloadquellen",
|
||||
"title_confirmation_delete_all_sources": "Löschen du alle Downloadquellen",
|
||||
"removed_download_sources": "Download-Quellen entfernt",
|
||||
"button_delete_all_sources": "Entfernen Sie alle Downloadquellen",
|
||||
"added_download_source": "Download-Quelle hinzugefügt",
|
||||
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
|
||||
"insert_valid_json_url": "Füge eine gültige JSON URL ein",
|
||||
|
||||
@@ -44,7 +44,10 @@
|
||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
|
||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
|
||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Installation complete",
|
||||
"installation_complete_message": "Common redistributables installed successfully"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filter…",
|
||||
@@ -178,6 +181,8 @@
|
||||
"manage_files_description": "Manage which files will be backed up and restored",
|
||||
"select_folder": "Select folder",
|
||||
"backup_from": "Backup from {{date}}",
|
||||
"automatic_backup_from": "Automatic backup from {{date}}",
|
||||
"enable_automatic_cloud_sync": "Enable automatic cloud sync",
|
||||
"custom_backup_location_set": "Custom backup location set",
|
||||
"no_directory_selected": "No directory selected",
|
||||
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
||||
@@ -191,7 +196,8 @@
|
||||
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available.",
|
||||
"game_removed_from_favorites": "Game removed from favorites",
|
||||
"game_added_to_favorites": "Game added to favorites"
|
||||
"game_added_to_favorites": "Game added to favorites",
|
||||
"automatically_extract_downloaded_files": "Automatically extract downloaded files"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@@ -228,7 +234,9 @@
|
||||
"seeding": "Seeding",
|
||||
"stop_seeding": "Stop seeding",
|
||||
"resume_seeding": "Resume seeding",
|
||||
"options": "Manage"
|
||||
"options": "Manage",
|
||||
"extract": "Extract files",
|
||||
"extracting": "Extracting files…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads path",
|
||||
@@ -265,6 +273,12 @@
|
||||
"download_source_errored": "Errored",
|
||||
"sync_download_sources": "Sync sources",
|
||||
"removed_download_source": "Download source removed",
|
||||
"removed_download_sources": "Download sources removed",
|
||||
"cancel_button_confirmation_delete_all_sources": "No",
|
||||
"confirm_button_confirmation_delete_all_sources": "Yes, delete everything",
|
||||
"title_confirmation_delete_all_sources": "Delete all download sources",
|
||||
"description_confirmation_delete_all_sources": "You will delete all download sources",
|
||||
"button_delete_all_sources": "Remove all",
|
||||
"added_download_source": "Added download source",
|
||||
"download_sources_synced": "All download sources are synced",
|
||||
"insert_valid_json_url": "Insert a valid JSON url",
|
||||
@@ -327,12 +341,20 @@
|
||||
"enable_torbox": "Enable Torbox",
|
||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||
"torbox_account_linked": "TorBox account linked",
|
||||
"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",
|
||||
"real_debrid_account_linked": "Real-Debrid account linked",
|
||||
"name_min_length": "Theme name must be at least 3 characters long",
|
||||
"import_theme": "Import theme",
|
||||
"import_theme_description": "You will import {{theme}} from the theme store",
|
||||
"error_importing_theme": "Error importing theme",
|
||||
"theme_imported": "Theme imported successfully"
|
||||
"theme_imported": "Theme imported successfully",
|
||||
"enable_friend_request_notifications": "When a friend request is received",
|
||||
"enable_auto_install": "Download updates automatically",
|
||||
"common_redist": "Common redistributables",
|
||||
"common_redist_description": "Common redistributables are required to run some games. Installing them is recommended to avoid issues.",
|
||||
"install_common_redist": "Install",
|
||||
"installing_common_redist": "Installing…"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
@@ -343,13 +365,19 @@
|
||||
"new_update_available": "Version {{version}} available",
|
||||
"restart_to_install_update": "Restart Hydra to install the update",
|
||||
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked"
|
||||
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
|
||||
"new_friend_request_description": "You have received a new friend request",
|
||||
"new_friend_request_title": "New friend request",
|
||||
"extraction_complete": "Extraction complete",
|
||||
"game_extracted": "{{title}} extracted successfully"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Open Hydra",
|
||||
"quit": "Quit"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Available",
|
||||
"available_other": "Available",
|
||||
"no_downloads": "No downloads available"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -444,9 +472,6 @@
|
||||
"show_achievements_on_profile": "Show your achievements on your profile",
|
||||
"show_points_on_profile": "Show your earned points on your profile"
|
||||
},
|
||||
"badge": {
|
||||
"badge_description_theme_creator": "Awarded to those who created a custom theme"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Achievement unlocked",
|
||||
"user_achievements": "{{displayName}}'s Achievements",
|
||||
|
||||
@@ -174,6 +174,8 @@
|
||||
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
||||
"select_folder": "Seleccionar carpeta",
|
||||
"backup_from": "Copia de seguridad de {{date}}",
|
||||
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
|
||||
"enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube",
|
||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||
"clear": "Limpiar",
|
||||
"no_directory_selected": "No se seleccionó un directorio",
|
||||
@@ -185,7 +187,13 @@
|
||||
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||
"reset_achievements_title": "¿Estás seguro?",
|
||||
"reset_achievements_success": "Logros reiniciados exitosamente",
|
||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
|
||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros",
|
||||
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
|
||||
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
|
||||
"download_error_not_cached_in_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
||||
"download_error_not_cached_in_torbox": "Esta descarga no está disponible en Torbox y el estado de descarga del sondeo aún no está disponible.",
|
||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||
"game_removed_from_favorites": "Juego removido de favoritos"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activar Hydra",
|
||||
@@ -259,6 +267,12 @@
|
||||
"download_source_errored": "Error",
|
||||
"sync_download_sources": "Sincronizar fuentes",
|
||||
"removed_download_source": "Fuente de descarga eliminada",
|
||||
"cancel_button_confirmation_delete_all_sources": "No",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sí, eliminar todo",
|
||||
"description_confirmation_delete_all_sources": "Eliminarás todas las fuentes de descarga",
|
||||
"title_confirmation_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
||||
"removed_download_sources": "Fuentes de descarga eliminadas",
|
||||
"button_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
||||
"added_download_source": "Fuente de descarga añadida",
|
||||
"download_sources_synced": "Todas las fuentes de descargas están actualizadas.",
|
||||
"insert_valid_json_url": "Introduce una URL JSON válida",
|
||||
@@ -290,14 +304,45 @@
|
||||
"no_email_account": "No has configurado un correo aún",
|
||||
"no_subscription": "Disfruta Hydra de la mejor manera",
|
||||
"no_users_blocked": "No tienes usuarios bloqueados",
|
||||
"notifications": "Notificaciones",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
|
||||
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
|
||||
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||
"update_email": "Actualizar correo",
|
||||
"update_password": "Actualizar contraseña"
|
||||
"update_password": "Actualizar contraseña",
|
||||
"appearance": "Apariencia",
|
||||
"become_subscriber": "Sé Hydra Cloud",
|
||||
"cancel": "Cancelar",
|
||||
"clear_themes": "Limpiar",
|
||||
"create_theme": "Crear",
|
||||
"create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra",
|
||||
"create_theme_modal_title": "Crear tema personalizado",
|
||||
"delete_all_themes": "Eliminar todos los temas",
|
||||
"delete_all_themes_description": "Esto eliminará todos tus temas personalizados",
|
||||
"delete_theme": "Eliminar tema",
|
||||
"delete_theme_description": "Esto eliminará el tema {{theme}}",
|
||||
"edit_theme": "Editar tema",
|
||||
"editor_tab_code": "Código",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Guardar",
|
||||
"enable_torbox": "Habilitar Torbox",
|
||||
"error_importing_theme": "Error al importar el tema",
|
||||
"import_theme": "Importar tema",
|
||||
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
|
||||
"insert_theme_name": "Introducí el nombre del tema",
|
||||
"name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo",
|
||||
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.",
|
||||
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
|
||||
"set_theme": "Establecer tema",
|
||||
"theme_imported": "Tema importado exitosamente",
|
||||
"theme_name": "Nombre",
|
||||
"torbox_account_linked": "Cuenta de TorBox vinculada",
|
||||
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
|
||||
"unset_theme": "Desactivar tema",
|
||||
"web_store": "Tienda Web",
|
||||
"enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad",
|
||||
"enable_auto_install": "Descargar actualizaciones automáticamente"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Descarga completada",
|
||||
@@ -308,13 +353,17 @@
|
||||
"new_update_available": "Version {{version}} disponible",
|
||||
"restart_to_install_update": "Reinicia Hydra para instalar la actualización",
|
||||
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados"
|
||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
||||
"new_friend_request_title": "Nueva solicitud de amistad",
|
||||
"new_friend_request_description": "Has recibido una nueva solicitud de amistad"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
"quit": "Salir"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Disponible",
|
||||
"available_other": "Disponibles",
|
||||
"no_downloads": "No hay descargas disponibles"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
|
||||
@@ -236,6 +236,12 @@
|
||||
"download_source_errored": "Vigane",
|
||||
"sync_download_sources": "Sünkroniseeri allikad",
|
||||
"removed_download_source": "Allalaadimise allikas eemaldatud",
|
||||
"cancel_button_confirmation_delete_all_sources": "Ei",
|
||||
"confirm_button_confirmation_delete_all_sources": "Jah, kustuta kõik",
|
||||
"description_confirmation_delete_all_sources": "Kustutate kõik allalaadimisallikad",
|
||||
"title_confirmation_delete_all_sources": "Kustutage kõik allalaadimisallikad",
|
||||
"removed_download_sources": "Allalaadimise allikas eemaldati",
|
||||
"button_delete_all_sources": "Eemaldage kõik allalaadimisallikad",
|
||||
"added_download_source": "Allalaadimise allikas lisatud",
|
||||
"download_sources_synced": "Kõik allalaadimise allikad on sünkroniseeritud",
|
||||
"insert_valid_json_url": "Sisesta kehtiv JSON url",
|
||||
|
||||
@@ -111,6 +111,12 @@
|
||||
"launch_with_system": "زمانی که سیستم روشن میشود، هایدرا را باز کن",
|
||||
"general": "کلی",
|
||||
"behavior": "رفتار",
|
||||
"cancel_button_confirmation_delete_all_sources": "خیر",
|
||||
"confirm_button_confirmation_delete_all_sources": "ہاں، سب کچھ حذف کر دیں۔",
|
||||
"description_confirmation_delete_all_sources": "آپ ڈاؤن لوڈ کے تمام ذرائع کو حذف کر دیں گے۔",
|
||||
"title_confirmation_delete_all_sources": "تمام منابع دانلود را حذف کنید",
|
||||
"removed_download_sources": "منابع دانلود حذف شد",
|
||||
"button_delete_all_sources": "تمام منابع دانلود را حذف کنید",
|
||||
"enable_real_debrid": "فعالسازی Real-Debrid",
|
||||
"debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
||||
"save_changes": "ذخیره تغییرات"
|
||||
|
||||
@@ -99,7 +99,13 @@
|
||||
"notifications": "Notifications",
|
||||
"enable_download_notifications": "Quand un téléchargement est terminé",
|
||||
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
|
||||
"language": "Langue"
|
||||
"language": "Langue",
|
||||
"cancel_button_confirmation_delete_all_sources": "Non",
|
||||
"confirm_button_confirmation_delete_all_sources": "Oui, tout supprimer",
|
||||
"description_confirmation_delete_all_sources": "Vous supprimerez toutes les sources de téléchargement",
|
||||
"title_confirmation_delete_all_sources": "Supprimer toutes les sources de téléchargement",
|
||||
"removed_download_sources": "Sources de téléchargement supprimées",
|
||||
"button_delete_all_sources": "Supprimer toutes les sources de téléchargement"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Téléchargement terminé",
|
||||
|
||||
@@ -106,6 +106,12 @@
|
||||
"change": "Frissítés",
|
||||
"notifications": "Értesítések",
|
||||
"enable_download_notifications": "Amikor egy letöltés befejeződik",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nem",
|
||||
"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": "Törölje az összes letöltési forrást",
|
||||
"removed_download_sources": "Betűtípusok eltávolítva",
|
||||
"button_delete_all_sources": "Távolítsa el az összes letöltési forrást",
|
||||
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül"
|
||||
},
|
||||
"notifications": {
|
||||
|
||||
@@ -175,6 +175,12 @@
|
||||
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
||||
"validate_download_source": "Validasi",
|
||||
"remove_download_source": "Hapus",
|
||||
"cancel_button_confirmation_delete_all_sources": "TIDAK",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ya, hapus semuanya",
|
||||
"description_confirmation_delete_all_sources": "Anda akan menghapus semua sumber unduhan",
|
||||
"title_confirmation_delete_all_sources": "Hapus semua sumber unduhan",
|
||||
"removed_download_sources": "Font dihapus",
|
||||
"button_delete_all_sources": "Hapus semua sumber unduhan",
|
||||
"add_download_source": "Tambahkan sumber",
|
||||
"download_count_zero": "Tidak ada unduhan dalam daftar",
|
||||
"download_count_one": "{{countFormatted}} unduhan dalam daftar",
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
"general": "Generale",
|
||||
"behavior": "Comportamento",
|
||||
"enable_real_debrid": "Abilita Real Debrid",
|
||||
"cancel_button_confirmation_delete_all_sources": "NO",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sì, cancella tutto",
|
||||
"description_confirmation_delete_all_sources": "Eliminerai tutte le fonti di download",
|
||||
"title_confirmation_delete_all_sources": "Elimina tutte le fonti di download",
|
||||
"removed_download_sources": "Fonti di download rimosse",
|
||||
"button_delete_all_sources": "Rimuovi tutte le fonti di download",
|
||||
"debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
||||
"save_changes": "Salva modifiche"
|
||||
},
|
||||
|
||||
@@ -173,6 +173,12 @@
|
||||
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
||||
"validate_download_source": "Тексеру",
|
||||
"remove_download_source": "Жою",
|
||||
"cancel_button_confirmation_delete_all_sources": "Жоқ",
|
||||
"confirm_button_confirmation_delete_all_sources": "Иә, бәрін жойыңыз",
|
||||
"description_confirmation_delete_all_sources": "Барлық жүктеу көздерін жоясыз",
|
||||
"title_confirmation_delete_all_sources": "Барлық жүктеу көздерін жойыңыз",
|
||||
"removed_download_sources": "Қаріптер жойылды",
|
||||
"button_delete_all_sources": "Барлық жүктеу көздерін жойыңыз",
|
||||
"add_download_source": "Жүктеу көзін қосу",
|
||||
"download_count_zero": "Жүктеулер тізімінде жоқ",
|
||||
"download_count_one": "{{countFormatted}} жүктеу тізімде",
|
||||
|
||||
@@ -111,6 +111,12 @@
|
||||
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
|
||||
"general": "일반",
|
||||
"behavior": "행동",
|
||||
"cancel_button_confirmation_delete_all_sources": "아니요",
|
||||
"confirm_button_confirmation_delete_all_sources": "네, 모두 삭제합니다",
|
||||
"description_confirmation_delete_all_sources": "모든 다운로드 소스를 삭제합니다.",
|
||||
"title_confirmation_delete_all_sources": "모든 다운로드 소스 삭제",
|
||||
"removed_download_sources": "제거된 글꼴",
|
||||
"button_delete_all_sources": "모든 다운로드 소스 제거",
|
||||
"enable_real_debrid": "Real-Debrid 활성화",
|
||||
"debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
||||
"save_changes": "변경 사항 저장"
|
||||
|
||||
@@ -201,6 +201,12 @@
|
||||
"download_source_errored": "Mislyktes",
|
||||
"sync_download_sources": "Synkroniser kilder",
|
||||
"removed_download_source": "Nedlastingskilde fjernet",
|
||||
"cancel_button_confirmation_delete_all_sources": "Ingen",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, slett alt",
|
||||
"description_confirmation_delete_all_sources": "Du vil slette alle nedlastingskilder",
|
||||
"title_confirmation_delete_all_sources": "Slett alle nedlastingskilder",
|
||||
"removed_download_sources": "Fonter fjernet",
|
||||
"button_delete_all_sources": "Fjern alle nedlastingskilder",
|
||||
"added_download_source": "La til Nedlastingskilde",
|
||||
"download_sources_synced": "Alle nedlastingskilder er synkroniserte",
|
||||
"insert_valid_json_url": "Innsett en gyldig JSON url",
|
||||
|
||||
@@ -112,6 +112,12 @@
|
||||
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
|
||||
"general": "Algemeen",
|
||||
"behavior": "Gedrag",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nee",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, verwijder alles",
|
||||
"description_confirmation_delete_all_sources": "Je verwijdert alle downloadbronnen",
|
||||
"title_confirmation_delete_all_sources": "Verwijder alle downloadbronnen",
|
||||
"removed_download_sources": "Downloadbronnen verwijderd",
|
||||
"button_delete_all_sources": "Verwijder alle downloadbronnen",
|
||||
"enable_real_debrid": "Enable Real-Debrid",
|
||||
"debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
||||
"save_changes": "Wijzigingen opslaan"
|
||||
|
||||
@@ -120,6 +120,11 @@
|
||||
"general": "Ogólne",
|
||||
"behavior": "Zachowania",
|
||||
"language": "Język",
|
||||
"cancel_button_confirmation_delete_all_sources": "NIE",
|
||||
"confirm_button_confirmation_delete_all_sources": "Tak, usuń wszystko",
|
||||
"description_confirmation_delete_all_sources": "Usuniesz wszystkie źródła pobierania",
|
||||
"title_confirmation_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
||||
"button_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
||||
"enable_real_debrid": "Włącz Real-Debrid",
|
||||
"debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
||||
"save_changes": "Zapisz zmiany"
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Downloads",
|
||||
"search_results": "Resultados da busca",
|
||||
@@ -45,7 +44,10 @@
|
||||
"downloading_metadata": "Baixando metadados de {{title}}…",
|
||||
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…",
|
||||
"checking_files": "Verificando arquivos de {{title}}…"
|
||||
"checking_files": "Verificando arquivos de {{title}}…",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Instalação concluída",
|
||||
"installation_complete_message": "Componentes recomendados instalados com sucesso"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Ver opções de download",
|
||||
@@ -165,6 +167,8 @@
|
||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
|
||||
"backup_from": "Backup de {{date}}",
|
||||
"automatic_backup_from": "Backup automático de {{date}}",
|
||||
"enable_automatic_cloud_sync": "Habilitar sincronização automática na nuvem",
|
||||
"custom_backup_location_set": "Localização customizada selecionada",
|
||||
"select_folder": "Selecione a pasta",
|
||||
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
||||
@@ -181,7 +185,8 @@
|
||||
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
|
||||
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos"
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos",
|
||||
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -218,7 +223,9 @@
|
||||
"seeding": "Semeando",
|
||||
"stop_seeding": "Parar de semear",
|
||||
"resume_seeding": "Semear",
|
||||
"options": "Gerenciar"
|
||||
"options": "Gerenciar",
|
||||
"extract": "Extrair arquivos",
|
||||
"extracting": "Extraindo arquivos…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
@@ -255,6 +262,12 @@
|
||||
"download_source_errored": "Falhou",
|
||||
"sync_download_sources": "Sincronizar",
|
||||
"removed_download_source": "Fonte removida",
|
||||
"removed_download_sources": "Fontes removidas",
|
||||
"cancel_button_confirmation_delete_all_sources": "Não",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo",
|
||||
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
|
||||
"description_confirmation_delete_all_sources": "Você irá remover todas as fontes de download. Deseja prosseguir?",
|
||||
"button_delete_all_sources": "Remover todas",
|
||||
"added_download_source": "Fonte adicionada",
|
||||
"download_sources_synced": "As fontes foram sincronizadas",
|
||||
"insert_valid_json_url": "Insira a url de um JSON válido",
|
||||
@@ -315,12 +328,20 @@
|
||||
"enable_torbox": "Habilitar Torbox",
|
||||
"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",
|
||||
"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",
|
||||
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
||||
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
||||
"import_theme": "Importar tema",
|
||||
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
|
||||
"error_importing_theme": "Erro ao importar tema",
|
||||
"theme_imported": "Tema importado com sucesso"
|
||||
"theme_imported": "Tema importado com sucesso",
|
||||
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
|
||||
"enable_auto_install": "Baixar atualizações automaticamente",
|
||||
"common_redist": "Componentes recomendados",
|
||||
"common_redist_description": "Componentes recomendados são necessários para executar alguns jogos. A instalação deles é recomendada para evitar problemas.",
|
||||
"install_common_redist": "Instalar",
|
||||
"installing_common_redist": "Instalando…"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
@@ -329,13 +350,19 @@
|
||||
"repack_count_one": "{{count}} novo repack",
|
||||
"repack_count_other": "{{count}} novos repacks",
|
||||
"new_update_available": "Versão {{version}} disponível",
|
||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
|
||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
|
||||
"new_friend_request_title": "Novo pedido de amizade",
|
||||
"new_friend_request_description": "Você recebeu um novo pedido de amizade",
|
||||
"extraction_complete": "Extração concluída",
|
||||
"game_extracted": "{{title}} extraído com sucesso"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
"quit": "Fechar"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Disponível",
|
||||
"available_other": "Disponíveis",
|
||||
"no_downloads": "Sem downloads disponíveis"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -440,9 +467,6 @@
|
||||
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
|
||||
},
|
||||
"badge": {
|
||||
"badge_description_theme_creator": "Concedido àqueles que criaram um tema customizado"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Conquista desbloqueada",
|
||||
"your_achievements": "Suas Conquistas",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Procurar jogos",
|
||||
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Transferências",
|
||||
"search_results": "Resultados da pesquisa",
|
||||
@@ -103,7 +102,7 @@
|
||||
"open_download_location": "Ver ficheiros transferidos",
|
||||
"create_shortcut": "Criar atalho no ambiente de trabalho",
|
||||
"remove_files": "Remover ficheiros",
|
||||
"options": "Gerir",
|
||||
"options": "Opções",
|
||||
"remove_from_library_description": "Isto vai remover {{game}} da tua biblioteca",
|
||||
"remove_from_library_title": "Tens a certeza?",
|
||||
"executable_section_title": "Executável",
|
||||
@@ -159,7 +158,27 @@
|
||||
"no_download_option_info": "Sem informações disponíveis",
|
||||
"backup_deletion_failed": "Falha ao apagar o backup",
|
||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||
"achievements_not_sync": "As tuas conquistas não estão sincronizadas"
|
||||
"achievements_not_sync": "As tuas conquistas não estão sincronizadas",
|
||||
"backup_from": "Backup de {{date}}",
|
||||
"automatic_backup_from": "Backup automático de {{date}}",
|
||||
"enable_automatic_cloud_sync": "Ativar sincronização automática na nuvem",
|
||||
"custom_backup_location_set": "Localização customizada selecionada",
|
||||
"select_folder": "Selecione a pasta",
|
||||
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
||||
"clear": "Limpar",
|
||||
"no_directory_selected": "Nenhum diretório selecionado",
|
||||
"reset_achievements": "Repor conquistas",
|
||||
"reset_achievements_description": "Isto irá apagar todas as conquistas de {{game}}",
|
||||
"reset_achievements_title": "Tem certeza?",
|
||||
"reset_achievements_success": "Conquistas repostas com sucesso",
|
||||
"reset_achievements_error": "Falha ao repor conquistas",
|
||||
"no_write_permission": "Não é possível descarregar neste diretório. Clique aqui para saber mais.",
|
||||
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde o reset da cota.",
|
||||
"download_error_real_debrid_account_not_authorized": "A sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique a sua assinatura e tente novamente.",
|
||||
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
|
||||
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -192,7 +211,11 @@
|
||||
"queued": "Na fila",
|
||||
"no_downloads_title": "Nada por aqui…",
|
||||
"no_downloads_description": "Ainda não descarregaste nada pelo Hydra, mas nunca é tarde para começar.",
|
||||
"checking_files": "A verificar ficheiros…"
|
||||
"checking_files": "A verificar ficheiros…",
|
||||
"seeding": "A semear",
|
||||
"stop_seeding": "Parar de semear",
|
||||
"resume_seeding": "Semear",
|
||||
"options": "Opções"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Local das transferências",
|
||||
@@ -232,6 +255,12 @@
|
||||
"download_source_errored": "Falhou",
|
||||
"sync_download_sources": "Sincronizar",
|
||||
"removed_download_source": "Fonte removida",
|
||||
"cancel_button_confirmation_delete_all_sources": "Não",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sim, apague tudo",
|
||||
"description_confirmation_delete_all_sources": "Irá remover todas as fontes de download",
|
||||
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
|
||||
"removed_download_sources": "Fontes de download removidas",
|
||||
"button_delete_all_sources": "Remover todas",
|
||||
"added_download_source": "Fonte adicionada",
|
||||
"download_sources_synced": "As fontes foram sincronizadas",
|
||||
"insert_valid_json_url": "Insere o URL de um JSON válido",
|
||||
@@ -250,7 +279,57 @@
|
||||
"must_be_valid_url": "A fonte deve ser um URL válido",
|
||||
"blocked_users": "Utilizadores bloqueados",
|
||||
"user_unblocked": "Utilizador desbloqueado",
|
||||
"enable_achievement_notifications": "Quando uma conquista é desbloqueada"
|
||||
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
|
||||
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
|
||||
"launch_minimized": "Iniciar Hydra minimizado",
|
||||
"disable_nsfw_alert": "Desativar alertas NSFW",
|
||||
"seed_after_download_complete": "Semear após concluir o download",
|
||||
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de as desbloquear",
|
||||
"account": "Conta",
|
||||
"no_users_blocked": "Não tem utilizadores bloqueados",
|
||||
"subscription_active_until": "O teu Hydra Cloud está ativo até {{date}}",
|
||||
"manage_subscription": "Gerir subscrições",
|
||||
"update_email": "Atualizar email",
|
||||
"update_password": "Atualizar password",
|
||||
"current_email": "Email atual:",
|
||||
"no_email_account": "Ainda não adicionou nenhum email à sua conta",
|
||||
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_expired_at": "A sua subscrição expirou a {{date}}",
|
||||
"no_subscription": "Aproveite o Hydra da melhor forma possível",
|
||||
"become_subscriber": "Subscreva o Hydra Cloud",
|
||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||
"bill_sent_until": "A próxima cobrança será enviada até esse dia",
|
||||
"no_themes": "Parece que ainda não tem nenhum tema. Não se preocupe, clique aqui para criar a sua primeira obra de arte.",
|
||||
"editor_tab_code": "Código",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Guardar",
|
||||
"web_store": "Loja de temas",
|
||||
"clear_themes": "Limpar",
|
||||
"create_theme": "Criar",
|
||||
"create_theme_modal_title": "Criar tema customizado",
|
||||
"create_theme_modal_description": "Criar um novo tema para customizar a aparência do Hydra",
|
||||
"theme_name": "Nome",
|
||||
"insert_theme_name": "Insira o nome do tema",
|
||||
"set_theme": "Definir tema",
|
||||
"unset_theme": "Redefinir tema",
|
||||
"delete_theme": "Apagar tema",
|
||||
"edit_theme": "Editar tema",
|
||||
"delete_all_themes": "Apagar todos os temas",
|
||||
"delete_all_themes_description": "Isto irá apagar todos os seus temas",
|
||||
"delete_theme_description": "Isto irá apagar o tema {{theme}}",
|
||||
"cancel": "Cancelar",
|
||||
"appearance": "Aparência",
|
||||
"enable_torbox": "Ativar Torbox",
|
||||
"torbox_description": "TorBox é um serviço de seedbox premium sendo um dos melhores servidores do mercado.",
|
||||
"torbox_account_linked": "Conta do TorBox associada",
|
||||
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
||||
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
||||
"import_theme": "Importar tema",
|
||||
"import_theme_description": "Irá importar {{theme}} da loja de temas",
|
||||
"error_importing_theme": "Erro ao importar tema",
|
||||
"theme_imported": "Tema importado com sucesso"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Transferência concluída",
|
||||
@@ -259,13 +338,17 @@
|
||||
"repack_count_one": "{{count}} novo repack",
|
||||
"repack_count_other": "{{count}} novos repacks",
|
||||
"new_update_available": "Versão {{version}} disponível",
|
||||
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão"
|
||||
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
|
||||
"new_friend_request_title": "Novo pedido de amizade",
|
||||
"new_friend_request_description": "Recebeste um novo pedido de amizade"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir o Hydra",
|
||||
"quit": "Sair"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Disponível",
|
||||
"available_other": "Disponíveis",
|
||||
"no_downloads": "Sem downloads disponíveis"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -275,7 +358,16 @@
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Página seguinte",
|
||||
"previous_page": "Página anterior"
|
||||
"previous_page": "Página anterior",
|
||||
"search": "Filtrar…",
|
||||
"developers": "Desenvolvedores",
|
||||
"genres": "Géneros",
|
||||
"tags": "Marcadores",
|
||||
"publishers": "Distribuidoras",
|
||||
"download_sources": "Fontes de download",
|
||||
"result_count": "{{resultCount}} resultados",
|
||||
"filter_count": "{{filterCount}} disponíveis",
|
||||
"clear_filters": "Limpar {{filterCount}} selecionados"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Botão de fechar"
|
||||
@@ -352,7 +444,17 @@
|
||||
"profile_reported": "Perfil denunciado",
|
||||
"your_friend_code": "O teu código de amigo:",
|
||||
"upload_banner": "Fazer upload do banner",
|
||||
"uploading_banner": "A fazer upload do banner…"
|
||||
"uploading_banner": "A fazer upload do banner…",
|
||||
"background_image_updated": "Imagem de fundo salva",
|
||||
"stats": "Estatísticas",
|
||||
"achievements": "conquistas",
|
||||
"games": "Jogos",
|
||||
"ranking_updated_weekly": "O ranking é atualizado semanalmente",
|
||||
"playing": "A jogar {{game}}",
|
||||
"achievements_unlocked": "Conquistas desbloqueadas",
|
||||
"earned_points": "Pontos ganhos",
|
||||
"show_achievements_on_profile": "Mostre as suas conquistas no perfil",
|
||||
"show_points_on_profile": "Mostre os seus pontos ganhos no perfil"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Conquista desbloqueada",
|
||||
@@ -360,15 +462,25 @@
|
||||
"user_achievements": "Conquistas de {{displayName}}",
|
||||
"unlocked_at": "Desbloqueada em: {{date}}",
|
||||
"subscription_needed": "Precisas de uma subscrição Hydra Cloud para visualizar este conteúdo",
|
||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
|
||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
||||
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Esta é uma conquista oculta",
|
||||
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
|
||||
"earned_points": "Pontos ganhos:",
|
||||
"available_points": "Pontos disponíveis:",
|
||||
"how_to_earn_achievements_points": "Como desbloquear pontos nas conquistas?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Subscrição Hydra Cloud",
|
||||
"subscribe_now": "Subscreve agora",
|
||||
"cloud_achievements": "Gravação de conquistas na nuvem",
|
||||
"animated_profile_picture": "Fotos de perfil animadas",
|
||||
"premium_support": "Apoio Premium",
|
||||
"premium_support": "Suporte Premium",
|
||||
"show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores",
|
||||
"animated_profile_banner": "Banner animado no perfil"
|
||||
"animated_profile_banner": "Banner animado no perfil",
|
||||
"cloud_saving": "Progresso dos jogos na nuvem",
|
||||
"hydra_cloud_feature_found": "Descubriste uma funcionalidade Hydra Cloud!",
|
||||
"learn_more": "Saber mais"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"behavior": "Comportament",
|
||||
"language": "Limbă",
|
||||
"api_token": "Token API",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nu",
|
||||
"confirm_button_confirmation_delete_all_sources": "Da, șterge totul",
|
||||
"description_confirmation_delete_all_sources": "Veți șterge toate sursele de descărcare",
|
||||
"title_confirmation_delete_all_sources": "Ștergeți toate sursele de descărcare",
|
||||
"removed_download_sources": "Sursele de descărcare au fost eliminate",
|
||||
"button_delete_all_sources": "Eliminați toate sursele de descărcare",
|
||||
"enable_real_debrid": "Activează Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
|
||||
"debrid_invalid_token": "Token API invalid",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"hot": "Сейчас популярно",
|
||||
"start_typing": "Начинаю вводить текст...",
|
||||
"weekly": "📅 Лучшие игры недели",
|
||||
"achievements": "🏆 Игры, в которых нужно победить"
|
||||
"achievements": "🏆 Игры с достижениями"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Каталог",
|
||||
@@ -36,7 +36,7 @@
|
||||
"downloads": "Загрузки",
|
||||
"search_results": "Результаты поиска",
|
||||
"settings": "Настройки",
|
||||
"version_available_install": "Доступна версия {{version}}. Нажмите здесь для перезапуска и установки.",
|
||||
"version_available_install": "Доступна версия {{version}}. Нажмите здесь для установки.",
|
||||
"version_available_download": "Доступна версия {{version}}. Нажмите здесь для загрузки."
|
||||
},
|
||||
"bottom_panel": {
|
||||
@@ -50,7 +50,7 @@
|
||||
"search": "Фильтр…",
|
||||
"developers": "Разработчики",
|
||||
"genres": "Жанры",
|
||||
"tags": "Маркеры",
|
||||
"tags": "Теги",
|
||||
"publishers": "Издательства",
|
||||
"download_sources": "Источники загрузки",
|
||||
"result_count": "{{resultCount}} результатов",
|
||||
@@ -178,16 +178,20 @@
|
||||
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
|
||||
"select_folder": "Выбрать папку",
|
||||
"backup_from": "Резервная копия от {{date}}",
|
||||
"automatic_backup_from": "Автоматическая резервная копия от {{date}}",
|
||||
"enable_automatic_cloud_sync": "Включить автоматическую синхронизацию в облаке",
|
||||
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
||||
"no_directory_selected": "Не выбран каталог",
|
||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||
"reset_achievements": "Сброс достижений",
|
||||
"reset_achievements_description": "Это сбросит все достижения для {{game}}",
|
||||
"reset_achievements_title": "Вы уверены?",
|
||||
"reset_achievements_success": "Достижения успешно сброшены",
|
||||
"reset_achievements_error": "Не удалось сбросить достижения",
|
||||
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
|
||||
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
|
||||
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, а опрос статуса загрузки с Real-Debrid пока недоступен.",
|
||||
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и опросить статус загрузки с Torbox пока невозможно.",
|
||||
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
|
||||
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и получить статус загрузки с Torbox пока невозможно.",
|
||||
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||
"game_removed_from_favorites": "Игра удалена из избранного"
|
||||
},
|
||||
@@ -265,9 +269,15 @@
|
||||
"download_source_up_to_date": "Обновлён",
|
||||
"download_source_errored": "Ошибка",
|
||||
"sync_download_sources": "Обновить источники",
|
||||
"removed_download_source": "Источник загрузок удален",
|
||||
"added_download_source": "Источник загрузок добавлен",
|
||||
"download_sources_synced": "Все источники загрузок синхронизированы",
|
||||
"removed_download_source": "Источник удален",
|
||||
"cancel_button_confirmation_delete_all_sources": "Нет",
|
||||
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
||||
"description_confirmation_delete_all_sources": "Вы удалите все источники",
|
||||
"title_confirmation_delete_all_sources": "Удалить все источники",
|
||||
"removed_download_sources": "Источники удалены",
|
||||
"button_delete_all_sources": "Удалить все источники",
|
||||
"added_download_source": "Источник добавлен",
|
||||
"download_sources_synced": "Все источники обновлены",
|
||||
"insert_valid_json_url": "Вставьте действительный URL JSON-файла",
|
||||
"found_download_option_zero": "Не найдено вариантов загрузки",
|
||||
"found_download_option_one": "Найден {{countFormatted}} вариант загрузки",
|
||||
@@ -275,7 +285,7 @@
|
||||
"import": "Импортировать",
|
||||
"blocked_users": "Заблокированные пользователи",
|
||||
"friends_only": "Только для друзей",
|
||||
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
|
||||
"must_be_valid_url": "У источника должен быть правильный URL",
|
||||
"privacy": "Конфиденциальность",
|
||||
"private": "Частный",
|
||||
"profile_visibility": "Видимость профиля",
|
||||
@@ -326,11 +336,15 @@
|
||||
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
|
||||
"torbox_account_linked": "Аккаунт TorBox привязан",
|
||||
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
|
||||
"create_real_debrid_account": "Нажмите здесь, если у вас еще нет аккаунта Real-Debrid",
|
||||
"create_torbox_account": "Нажмите здесь, если у вас еще нет учетной записи TorBox",
|
||||
"name_min_length": "Название темы должно содержать не менее 3 символов",
|
||||
"import_theme": "Импортировать тему",
|
||||
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
|
||||
"error_importing_theme": "Ошибка при импорте темы",
|
||||
"theme_imported": "Тема успешно импортирована"
|
||||
"theme_imported": "Тема успешно импортирована",
|
||||
"enable_friend_request_notifications": "При получении запроса на добавление в друзья",
|
||||
"enable_auto_install": "Загружать обновления автоматически"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
@@ -341,13 +355,17 @@
|
||||
"new_update_available": "Доступна новая версия {{version}}",
|
||||
"restart_to_install_update": "Перезапустите Hydra для установки обновления",
|
||||
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
|
||||
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}"
|
||||
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
|
||||
"new_friend_request_title": "Новый запрос на добавление в друзья",
|
||||
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Открыть Hydra",
|
||||
"quit": "Выйти"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Доступный",
|
||||
"available_other": "Доступный",
|
||||
"no_downloads": "Нет доступных источников"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -432,11 +450,13 @@
|
||||
"uploading_banner": "Загрузка баннера...",
|
||||
"background_image_updated": "Фоновое изображение обновлено",
|
||||
"stats": "Статистика",
|
||||
"achievements": "Достижения",
|
||||
"games": "Игры",
|
||||
"top_percentile": "Топ {{percentile}}%",
|
||||
"ranking_updated_weekly": "Рейтинг обновляется еженедельно",
|
||||
"playing": "Играет в {{game}}",
|
||||
"achievements_unlocked": "Достижения разблокированы",
|
||||
"earned_points": "Заработано очков:",
|
||||
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
||||
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirilenler",
|
||||
"settings": "Ayarlar",
|
||||
"my_library": "Kütüphanem",
|
||||
"my_library": "Kütüphane",
|
||||
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
|
||||
"paused": "{{title}} (Durduruldu)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
@@ -36,13 +36,13 @@
|
||||
"downloads": "İndirilenler",
|
||||
"search_results": "Arama sonuçları",
|
||||
"settings": "Ayarlar",
|
||||
"version_available_install": "Sürüm {{version}} mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "Sürüm {{version}} mevcut. İndirmek için buraya tıklayın."
|
||||
"version_available_install": "{{version}} sürümü mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "{{version}} sürümü mevcut. İndirmek için buraya tıklayın."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Devam eden indirme yok",
|
||||
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlama: {{eta}} - Hız: {{speed}}",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlanma: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)"
|
||||
},
|
||||
@@ -116,14 +116,14 @@
|
||||
"clear": "Temizle",
|
||||
"remove_files": "Dosyaları kaldır",
|
||||
"remove_from_library_title": "Emin misiniz?",
|
||||
"remove_from_library_description": "Bu işlem {{game}} oyununu kütüphanenizden kaldıracaktır",
|
||||
"remove_from_library_description": "Bu işlem sonrasında {{game}} oyunu kütüphanenizden kaldıracaktır",
|
||||
"options": "Seçenekler",
|
||||
"executable_section_title": "Çalıştırılabilir dosya",
|
||||
"executable_section_description": "\"Oyna\" tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"executable_section_description": "\"Oyna\" butonuna tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"downloads_secion_title": "İndirmeler",
|
||||
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
|
||||
"danger_zone_section_title": "Tehlike bölgesi",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyaları kaldırın",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden kaldırın veya Hydra tarafından indirilen dosyaları silin.",
|
||||
"download_in_progress": "İndirme devam ediyor",
|
||||
"download_paused": "İndirme durduruldu",
|
||||
"last_downloaded_option": "Son indirilen seçenek",
|
||||
@@ -141,8 +141,8 @@
|
||||
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
|
||||
"warning": "Uyarı:",
|
||||
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
|
||||
"achievements": "Başarılar",
|
||||
"achievements_count": "Başarılar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"achievements": "Başarımlar",
|
||||
"achievements_count": "Başarımlar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Bulut kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
|
||||
"backups": "Yedekler",
|
||||
@@ -150,15 +150,15 @@
|
||||
"delete_backup": "Sil",
|
||||
"create_backup": "Yeni yedek oluştur",
|
||||
"last_backup_date": "{{date}} tarihindeki son yedek",
|
||||
"no_backup_preview": "Bu oyun için kayıtlı oyun bulunamadı",
|
||||
"no_backup_preview": "Bu oyun için bir kayıt dosyası bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
|
||||
"uploading_backup": "Yedek yükleniyor…",
|
||||
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
|
||||
"backup_uploaded": "Yedek yüklendi",
|
||||
"backup_deleted": "Yedek silindi",
|
||||
"backup_restored": "Yedek geri yüklendi",
|
||||
"see_all_achievements": "Tüm başarıları gör",
|
||||
"sign_in_to_see_achievements": "Başarıları görmek için giriş yapın",
|
||||
"see_all_achievements": "Tüm başarımları gör",
|
||||
"sign_in_to_see_achievements": "Başarımları görmek için oturum açın",
|
||||
"mapping_method_automatic": "Otomatik",
|
||||
"mapping_method_manual": "Manuel",
|
||||
"mapping_method_label": "Eşleme yöntemi",
|
||||
@@ -169,29 +169,37 @@
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
|
||||
"launch_options": "Başlatma Seçenekleri",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine değişiklikler girebilir (deneysel özellik)",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine parametreler girebilir (deneysel özellik)",
|
||||
"launch_options_placeholder": "Belirtilen bir parametre yok",
|
||||
"no_download_option_info": "Bilgi mevcut değil",
|
||||
"backup_deletion_failed": "Yedek silinemedi",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarılarınızı senkronize etmeyi öğrenin",
|
||||
"achievements_not_sync": "Başarımlarınızı senkronize etmeyi öğrenin",
|
||||
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
|
||||
"select_folder": "Klasör seç",
|
||||
"backup_from": "{{date}} tarihinden yedek",
|
||||
"automatic_backup_from": "{{date}} tarihinden otomatik kayıt",
|
||||
"enable_automatic_cloud_sync": "Otomatik bulut kaydı senkronizasyonunu aktifleştir",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
|
||||
"no_directory_selected": "Bir dizin seçilmedi",
|
||||
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
|
||||
"reset_achievements": "Başarıları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarıları sıfırlar",
|
||||
"reset_achievements": "Başarımları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarımları sıfırlar",
|
||||
"reset_achievements_title": "Emin misiniz?",
|
||||
"reset_achievements_success": "Başarılar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarılar sıfırlanamadı"
|
||||
"reset_achievements_success": "Başarımlar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarımlar sıfırlanamadı",
|
||||
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı doldurdunuz. Kotanın yenilenmesini bekleyin.",
|
||||
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirme işlemleri yapmak için yetkilendirilmemiş. Lütfen hesap ayarlarınızı kontrol edip tekrar deneyin.",
|
||||
"download_error_not_cached_in_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve Real-Debrid'den indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_in_torbox": "Bu indirme Torbox'ta mevcut değil ve Torbox'tan indirme durumu henüz sorgulanamıyor.",
|
||||
"game_removed_from_favorites": "Oyun favorilerden silindi",
|
||||
"game_added_to_favorites": "Oyun favorilere eklendi"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'yı Aktive Et",
|
||||
"installation_id": "Kurulum Kimliği:",
|
||||
"enter_activation_code": "Aktivasyon kodunuzu girin",
|
||||
"message": "Bunu nereden soracağınızı bilmiyorsanız, bu sizin için olmamalı.",
|
||||
"message": "Bunu nasıl edineceğini bilmiyorsan, buna sahip olmamalısın.",
|
||||
"activate": "Aktive Et",
|
||||
"loading": "Yükleniyor…"
|
||||
},
|
||||
@@ -206,7 +214,7 @@
|
||||
"cancel": "İptal Et",
|
||||
"filter": "İndirilen oyunları filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"downloading_metadata": "Meta verileri indiriliyor…",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"delete": "Yükleyiciyi kaldır",
|
||||
"delete_modal_title": "Emin misiniz?",
|
||||
@@ -231,7 +239,7 @@
|
||||
"enable_download_notifications": "Bir indirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
|
||||
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatırken gizlemeyin",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatınca sistem tepsisine gitmesin",
|
||||
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
|
||||
"general": "Genel",
|
||||
"behavior": "Davranış",
|
||||
@@ -250,6 +258,12 @@
|
||||
"validate_download_source": "Doğrula",
|
||||
"remove_download_source": "Kaldır",
|
||||
"add_download_source": "Kaynak ekle",
|
||||
"cancel_button_confirmation_delete_all_sources": "Hayır",
|
||||
"confirm_button_confirmation_delete_all_sources": "Evet, her şeyi sil",
|
||||
"description_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sileceksiniz",
|
||||
"title_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sil",
|
||||
"removed_download_sources": "Yazı tipleri kaldırıldı",
|
||||
"button_delete_all_sources": "Tüm indirme kaynaklarını kaldır",
|
||||
"download_count_zero": "İndirme seçeneği yok",
|
||||
"download_count_one": "{{countFormatted}} indirme seçeneği",
|
||||
"download_count_other": "{{countFormatted}} indirme seçeneği",
|
||||
@@ -277,11 +291,56 @@
|
||||
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"user_unblocked": "Kullanıcının engeli kaldırıldı",
|
||||
"enable_achievement_notifications": "Bir başarı kilidi açıldığında",
|
||||
"enable_achievement_notifications": "Bir başarım kilidi açıldığında",
|
||||
"launch_minimized": "Hydra'yı küçültülmüş başlat",
|
||||
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
|
||||
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
|
||||
"show_hidden_achievement_description": "Gizli başarı açıklamalarını kilitlenmeden önce göster"
|
||||
"show_hidden_achievement_description": "Gizli başarım açıklamalarını kilitlenmeden önce göster",
|
||||
"account": "Hesap",
|
||||
"no_users_blocked": "Hiçbir kullanıcıyı engellemediniz",
|
||||
"subscription_active_until": "Hydra Cloud'unuz {{date}} tarihine kadar aktif",
|
||||
"manage_subscription": "Aboneliği yönet",
|
||||
"update_email": "E-posta'yı güncelle",
|
||||
"update_password": "Şifreyi güncelle",
|
||||
"current_email": "Aktif e-posta'nız",
|
||||
"no_email_account": "Henüz ayarlanmış bir e-postanız yok",
|
||||
"account_data_updated_successfully": "Hesap bilgileri başarıyla güncellendi",
|
||||
"renew_subscription": "Hydra Cloud'u yenile",
|
||||
"subscription_expired_at": "Aboneliğiniz {{date}} tarihinde sona erdi",
|
||||
"no_subscription": "Hydra'yı en iyi şekilde deneyimleyin",
|
||||
"become_subscriber": "Hydra Cloud'lu ol",
|
||||
"subscription_renew_cancelled": "Otomatik yenileme devre dışı",
|
||||
"subscription_renews_on": "Aboneliğiniz {{date}} tarihinde yenilenecek",
|
||||
"bill_sent_until": "Bir sonraki faturanız bu tarihe kadar gönderilecek",
|
||||
"no_themes": "Henüz bir temanız yok gibi görünüyor, ama endişelenmeyin, ilk şaheserinizi oluşturmak için buraya tıklayın.",
|
||||
"editor_tab_code": "Kod",
|
||||
"editor_tab_info": "Bilgi",
|
||||
"editor_tab_save": "Kaydet",
|
||||
"web_store": "İnternet mağazası",
|
||||
"clear_themes": "Temizle",
|
||||
"create_theme": "Oluştur",
|
||||
"create_theme_modal_title": "Tema oluştur",
|
||||
"create_theme_modal_description": "Hydra'nın görünümünü özelleştirmek için yeni bir tema oluştur",
|
||||
"theme_name": "İsim",
|
||||
"insert_theme_name": "Tema ismini gir",
|
||||
"set_theme": "Temayı seç",
|
||||
"unset_theme": "Tema seçimini kaldır",
|
||||
"delete_theme": "Temayı sil",
|
||||
"edit_theme": "Temayı düzenle",
|
||||
"delete_all_themes": "Tüm temaları sil",
|
||||
"delete_all_themes_description": "Bu tüm temalarınızı silecektir",
|
||||
"delete_theme_description": "Bu {{theme}} temasını silecektir",
|
||||
"cancel": "İptal",
|
||||
"appearance": "Görünüm",
|
||||
"enable_torbox": "Torbox'u etkinleştir",
|
||||
"torbox_description": "TorBox, piyasadaki en iyi sunucularla bile rekabet edebilen premium seedbox hizmetinizdir.",
|
||||
"torbox_account_linked": "TorBox hesabı bağlando",
|
||||
"real_debrid_account_linked": "Real-Debrid hesabı bağlando",
|
||||
"name_min_length": "Tema ismi en az 3 karakter uzunluğunda olmalıdır",
|
||||
"import_theme": "Temayı içe aktar",
|
||||
"import_theme_description": "{{theme}} teması, tema mağazasından içeri aktarılacak",
|
||||
"error_importing_theme": "Temayı içe aktarmada bir sorun oluştu",
|
||||
"theme_imported": "Tema başarıyla içe aktarıldı"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "İndirme tamamlandı",
|
||||
@@ -289,17 +348,19 @@
|
||||
"repack_list_updated": "Repack listesi güncellendi",
|
||||
"repack_count_one": "{{count}} repack eklendi",
|
||||
"repack_count_other": "{{count}} repack eklendi",
|
||||
"new_update_available": "Sürüm {{version}} mevcut",
|
||||
"new_update_available": "{{version}} sürümü mevcut",
|
||||
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarı kilidi açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarılar açıldı"
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarım kilidi açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarım açıldı"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra'yı Aç",
|
||||
"quit": "Çık"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "İndirilebilir içerik bulunmuyor"
|
||||
"no_downloads": "İndirilebilir içerik bulunmuyor",
|
||||
"available_one": "Mevcut",
|
||||
"available_other": "Mevcut"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programlar Yüklü Değil",
|
||||
@@ -332,7 +393,7 @@
|
||||
"successfully_signed_out": "Başarıyla çıkış yapıldı",
|
||||
"sign_out": "Çıkış yap",
|
||||
"playing_for": "{{amount}} oynanıyor",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaptığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Çıkışa devam etmek istiyor musunuz?",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Oturumu kapattığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Oturumu kapatmaya devam etmek istiyor musunuz?",
|
||||
"add_friends": "Arkadaş Ekle",
|
||||
"add": "Ekle",
|
||||
"friend_code": "Arkadaş kodu",
|
||||
@@ -383,39 +444,39 @@
|
||||
"uploading_banner": "Afiş yükleniyor…",
|
||||
"background_image_updated": "Arka plan görüntüsü güncellendi",
|
||||
"stats": "İstatistikler",
|
||||
"achievements": "Başarılar",
|
||||
"achievements": "Başarımlar",
|
||||
"games": "Oyunlar",
|
||||
"top_percentile": "En üst {{percentile}}%",
|
||||
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
|
||||
"playing": "{{game}} oynanıyor",
|
||||
"achievements_unlocked": "Başarılar açıldı",
|
||||
"achievements_unlocked": "Başarımlar açıldı",
|
||||
"earned_points": "Kazanılan puanlar",
|
||||
"show_achievements_on_profile": "Başarılarınızı profilinizde gösterin",
|
||||
"show_achievements_on_profile": "Başarımlarınızı profilinizde gösterin",
|
||||
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Başarı açıldı",
|
||||
"user_achievements": "{{displayName}}'in Başarıları",
|
||||
"your_achievements": "Başarılarınız",
|
||||
"achievement_unlocked": "Başarım açıldı",
|
||||
"user_achievements": "{{displayName}} oyununun Başarımları",
|
||||
"your_achievements": "Başarımlarınız",
|
||||
"unlocked_at": "Açılma zamanı: {{date}}",
|
||||
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarı açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarı açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
|
||||
"achievement_earn_points": "Bu başarı ile {{points}} puan kazanın",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarım açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarım",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarım açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarımdır",
|
||||
"achievement_earn_points": "Bu başarım ile {{points}} puan kazanın",
|
||||
"earned_points": "Kazanılan puanlar:",
|
||||
"available_points": "Mevcut puanlar:",
|
||||
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
|
||||
"how_to_earn_achievements_points": "Başarım puanları nasıl kazanılır?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud Aboneliği",
|
||||
"subscribe_now": "Şimdi abone olun",
|
||||
"cloud_saving": "Bulut kaydetme",
|
||||
"cloud_achievements": "Başarılarınızı buluta kaydedin",
|
||||
"cloud_achievements": "Başarımlarınızı buluta kaydedin",
|
||||
"animated_profile_picture": "Animasyonlu profil resimleri",
|
||||
"premium_support": "Premium Destek",
|
||||
"show_and_compare_achievements": "Başarılarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
|
||||
"show_and_compare_achievements": "Başarımlarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
|
||||
"animated_profile_banner": "Animasyonlu profil afişi",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
|
||||
|
||||
@@ -176,6 +176,12 @@
|
||||
"import": "Імпортувати",
|
||||
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
|
||||
"language": "Мова",
|
||||
"cancel_button_confirmation_delete_all_sources": "немає",
|
||||
"confirm_button_confirmation_delete_all_sources": "Так, видалити все",
|
||||
"description_confirmation_delete_all_sources": "Ви видалите всі джерела завантаження",
|
||||
"title_confirmation_delete_all_sources": "Видалити всі джерела завантаження",
|
||||
"removed_download_sources": "Джерела завантажень видалено",
|
||||
"button_delete_all_sources": "Видаліть усі джерела завантаження",
|
||||
"api_token": "API-токен",
|
||||
"debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||
|
||||
@@ -226,6 +226,12 @@
|
||||
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
||||
"validate_download_source": "验证",
|
||||
"remove_download_source": "移除",
|
||||
"cancel_button_confirmation_delete_all_sources": "不",
|
||||
"confirm_button_confirmation_delete_all_sources": "是的,删除所有内容",
|
||||
"description_confirmation_delete_all_sources": "您将删除所有下载源",
|
||||
"title_confirmation_delete_all_sources": "删除所有下载源",
|
||||
"removed_download_sources": "已删除字体",
|
||||
"button_delete_all_sources": "删除所有下载源",
|
||||
"add_download_source": "添加源",
|
||||
"download_count_zero": "列表中无下载",
|
||||
"download_count_one": "列表中有 {{countFormatted}} 个下载",
|
||||
|
||||
@@ -12,10 +12,9 @@ export const levelDatabasePath = path.join(
|
||||
`hydra-db${isStaging ? "-staging" : ""}`
|
||||
);
|
||||
|
||||
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||
export const databasePath = path.join(
|
||||
databaseDirectory,
|
||||
isStaging ? "hydra_test.db" : "hydra.db"
|
||||
export const commonRedistPath = path.join(
|
||||
app.getPath("userData"),
|
||||
"CommonRedist"
|
||||
);
|
||||
|
||||
export const logsPath = path.join(app.getPath("userData"), "logs");
|
||||
@@ -31,3 +30,5 @@ export const achievementSoundPath = app.isPackaged
|
||||
export const backupsPath = path.join(app.getPath("userData"), "Backups");
|
||||
|
||||
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
||||
|
||||
export const MAIN_LOOP_INTERVAL = 1500;
|
||||
|
||||
@@ -4,11 +4,15 @@ import updater from "electron-updater";
|
||||
|
||||
const { autoUpdater } = updater;
|
||||
|
||||
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
export const restartAndInstallUpdate = () => {
|
||||
autoUpdater.removeAllListeners();
|
||||
if (app.isPackaged) {
|
||||
autoUpdater.quitAndInstall(false);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("restartAndInstallUpdate", restartAndInstallUpdate);
|
||||
const restartAndInstallUpdateEvent = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => restartAndInstallUpdate();
|
||||
|
||||
registerEvent("restartAndInstallUpdate", restartAndInstallUpdateEvent);
|
||||
|
||||
@@ -1,44 +1,8 @@
|
||||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
||||
import { CloudSync } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import * as tar from "tar";
|
||||
import crypto from "node:crypto";
|
||||
import type { GameShop } from "@types";
|
||||
import axios from "axios";
|
||||
import os from "node:os";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { app } from "electron";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const bundleBackup = async (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix: string | null
|
||||
) => {
|
||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||
|
||||
// Remove existing backup
|
||||
if (fs.existsSync(backupPath)) {
|
||||
fs.rmSync(backupPath, { recursive: true });
|
||||
}
|
||||
|
||||
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||
|
||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||
|
||||
await tar.create(
|
||||
{
|
||||
gzip: false,
|
||||
file: tarLocation,
|
||||
cwd: backupPath,
|
||||
},
|
||||
["."]
|
||||
);
|
||||
|
||||
return tarLocation;
|
||||
};
|
||||
import i18next, { t } from "i18next";
|
||||
import { formatDate } from "date-fns";
|
||||
|
||||
const uploadSaveGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -46,61 +10,17 @@ const uploadSaveGame = async (
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null
|
||||
) => {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
const { language } = i18next;
|
||||
|
||||
const bundleLocation = await bundleBackup(
|
||||
shop,
|
||||
return CloudSync.uploadSaveGame(
|
||||
objectId,
|
||||
game?.winePrefixPath ?? null
|
||||
shop,
|
||||
downloadOptionTitle,
|
||||
t("backup_from", {
|
||||
ns: "game_details",
|
||||
date: formatDate(new Date(), language),
|
||||
})
|
||||
);
|
||||
|
||||
fs.stat(bundleLocation, async (err, stat) => {
|
||||
if (err) {
|
||||
logger.error("Failed to get zip file stats", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { uploadUrl } = await HydraApi.post<{
|
||||
id: string;
|
||||
uploadUrl: string;
|
||||
}>("/profile/games/artifacts", {
|
||||
artifactLengthInBytes: stat.size,
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(app.getPath("home")),
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
});
|
||||
|
||||
fs.readFile(bundleLocation, async (err, fileBuffer) => {
|
||||
if (err) {
|
||||
logger.error("Failed to read zip file", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
await axios.put(uploadUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/tar",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
logger.log(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-upload-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(bundleLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to remove tar file", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("uploadSaveGame", uploadSaveGame);
|
||||
|
||||
13
src/main/events/download-sources/create-download-sources.ts
Normal file
13
src/main/events/download-sources/create-download-sources.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const createDownloadSources = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
urls: string[]
|
||||
) => {
|
||||
await HydraApi.post("/profile/download-sources", {
|
||||
urls,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("createDownloadSources", createDownloadSources);
|
||||
8
src/main/events/download-sources/get-download-sources.ts
Normal file
8
src/main/events/download-sources/get-download-sources.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get("/profile/download-sources");
|
||||
};
|
||||
|
||||
registerEvent("getDownloadSources", getDownloadSources);
|
||||
18
src/main/events/download-sources/remove-download-source.ts
Normal file
18
src/main/events/download-sources/remove-download-source.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const removeDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url?: string,
|
||||
removeAll = false
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
all: removeAll.toString(),
|
||||
});
|
||||
|
||||
if (url) params.set("url", url);
|
||||
|
||||
return HydraApi.delete(`/profile/download-sources?${params.toString()}`);
|
||||
};
|
||||
|
||||
registerEvent("removeDownloadSource", removeDownloadSource);
|
||||
@@ -20,6 +20,7 @@ import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
import "./library/get-game-by-object-id";
|
||||
import "./library/get-library";
|
||||
import "./library/extract-game-download";
|
||||
import "./library/open-game";
|
||||
import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
@@ -31,11 +32,15 @@ import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./library/select-game-wine-prefix";
|
||||
import "./library/reset-game-achievements";
|
||||
import "./library/toggle-automatic-cloud-sync";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
import "./misc/get-features";
|
||||
import "./misc/show-item-in-folder";
|
||||
import "./misc/get-badges";
|
||||
import "./misc/install-common-redist";
|
||||
import "./misc/can-install-common-redist";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./torrenting/resume-game-download";
|
||||
@@ -58,6 +63,7 @@ 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-user-stats";
|
||||
import "./user/report-user";
|
||||
import "./user/get-unlocked-achievements";
|
||||
@@ -87,6 +93,9 @@ import "./themes/get-custom-theme-by-id";
|
||||
import "./themes/get-active-custom-theme";
|
||||
import "./themes/close-editor-window";
|
||||
import "./themes/toggle-custom-theme";
|
||||
import "./download-sources/create-download-sources";
|
||||
import "./download-sources/remove-download-source";
|
||||
import "./download-sources/get-download-sources";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
|
||||
46
src/main/events/library/extract-game-download.ts
Normal file
46
src/main/events/library/extract-game-download.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import path from "node:path";
|
||||
import { GameFilesManager } from "@main/services";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
|
||||
|
||||
const extractGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
): Promise<boolean> => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const [download, game] = await Promise.all([
|
||||
downloadsSublevel.get(gameKey),
|
||||
gamesSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
if (!download || !game) return false;
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download,
|
||||
extracting: true,
|
||||
});
|
||||
|
||||
const gameFilesManager = new GameFilesManager(shop, objectId);
|
||||
|
||||
if (
|
||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) => download.folderName?.endsWith(ext))
|
||||
) {
|
||||
gameFilesManager.extractDownloadedFile();
|
||||
} else {
|
||||
gameFilesManager
|
||||
.extractFilesInDirectory(
|
||||
path.join(download.downloadPath, download.folderName!)
|
||||
)
|
||||
.then(() => {
|
||||
gameFilesManager.setExtractionComplete(false);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
registerEvent("extractGameDownload", extractGameDownload);
|
||||
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal file
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { levelKeys, gamesSublevel } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const toggleAutomaticCloudSync = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
automaticCloudSync: boolean
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
automaticCloudSync,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("toggleAutomaticCloudSync", toggleAutomaticCloudSync);
|
||||
@@ -21,6 +21,8 @@ const updateExecutablePath = async (
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
executablePath: parsedPath,
|
||||
automaticCloudSync:
|
||||
executablePath === null ? false : game.automaticCloudSync,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
7
src/main/events/misc/can-install-common-redist.ts
Normal file
7
src/main/events/misc/can-install-common-redist.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { CommonRedistManager } from "@main/services/common-redist-manager";
|
||||
|
||||
const canInstallCommonRedist = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
CommonRedistManager.canInstallCommonRedist();
|
||||
|
||||
registerEvent("canInstallCommonRedist", canInstallCommonRedist);
|
||||
22
src/main/events/misc/get-badges.ts
Normal file
22
src/main/events/misc/get-badges.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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: "utf-8",
|
||||
})
|
||||
.then((language) => language || "en");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
locale: language,
|
||||
});
|
||||
|
||||
return HydraApi.get<Badge[]>(`/badges?${params.toString()}`, null, {
|
||||
needsAuth: false,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("getBadges", getBadges);
|
||||
10
src/main/events/misc/install-common-redist.ts
Normal file
10
src/main/events/misc/install-common-redist.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { CommonRedistManager } from "@main/services/common-redist-manager";
|
||||
|
||||
const installCommonRedist = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
if (await CommonRedistManager.canInstallCommonRedist()) {
|
||||
CommonRedistManager.installCommonRedist();
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("installCommonRedist", installCommonRedist);
|
||||
@@ -1,17 +1,59 @@
|
||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { HydraApi, WindowManager } from "@main/services";
|
||||
import { publishNewFriendRequestNotification } from "@main/services/notifications";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import type { FriendRequestSync } from "@types";
|
||||
|
||||
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`).catch(
|
||||
(err) => {
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return { friendRequests: [] };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
interface SyncState {
|
||||
friendRequestCount: number | null;
|
||||
tick: number;
|
||||
}
|
||||
|
||||
const ticksToUpdate = (2 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 2 minutes
|
||||
|
||||
const syncState: SyncState = {
|
||||
friendRequestCount: null,
|
||||
tick: 0,
|
||||
};
|
||||
|
||||
registerEvent("syncFriendRequests", syncFriendRequests);
|
||||
const syncFriendRequests = async () => {
|
||||
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`)
|
||||
.then((res) => {
|
||||
if (
|
||||
syncState.friendRequestCount != null &&
|
||||
syncState.friendRequestCount < res.friendRequestCount
|
||||
) {
|
||||
publishNewFriendRequestNotification();
|
||||
}
|
||||
|
||||
syncState.friendRequestCount = res.friendRequestCount;
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-sync-friend-requests",
|
||||
res
|
||||
);
|
||||
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return { friendRequestCount: 0 } as FriendRequestSync;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
const syncFriendRequestsEvent = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return syncFriendRequests();
|
||||
};
|
||||
|
||||
export const watchFriendRequests = async () => {
|
||||
if (syncState.tick % ticksToUpdate === 0) {
|
||||
await syncFriendRequests();
|
||||
}
|
||||
|
||||
syncState.tick++;
|
||||
};
|
||||
|
||||
registerEvent("syncFriendRequests", syncFriendRequestsEvent);
|
||||
|
||||
@@ -12,7 +12,15 @@ const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
payload: StartGameDownloadPayload
|
||||
) => {
|
||||
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
||||
const {
|
||||
objectId,
|
||||
title,
|
||||
shop,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
automaticallyExtract,
|
||||
} = payload;
|
||||
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
@@ -74,6 +82,8 @@ const startGameDownload = async (
|
||||
shouldSeed: false,
|
||||
timestamp: Date.now(),
|
||||
queued: true,
|
||||
extracting: false,
|
||||
automaticallyExtract,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -23,10 +23,6 @@ const updateUserPreferences = async (
|
||||
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||
}
|
||||
|
||||
if (!preferences.downloadsPath) {
|
||||
preferences.downloadsPath = null;
|
||||
}
|
||||
|
||||
await db.put<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
|
||||
11
src/main/events/user/get-auth.ts
Normal file
11
src/main/events/user/get-auth.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getAuth = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
db.get<string, Auth>(levelKeys.auth, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
registerEvent("getAuth", getAuth);
|
||||
@@ -1,11 +0,0 @@
|
||||
import knex from "knex";
|
||||
import { databasePath } from "./constants";
|
||||
import { app } from "electron";
|
||||
|
||||
export const knexClient = knex({
|
||||
debug: !app.isPackaged,
|
||||
client: "better-sqlite3",
|
||||
connection: {
|
||||
filename: databasePath,
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
const config = {
|
||||
development: {
|
||||
migrations: {
|
||||
extension: "ts",
|
||||
stub: "migrations/migration.stub",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -13,5 +13,5 @@ export const levelKeys = {
|
||||
downloads: "downloads",
|
||||
userPreferences: "userPreferences",
|
||||
language: "language",
|
||||
sqliteMigrationDone: "sqliteMigrationDone",
|
||||
screenState: "screenState",
|
||||
};
|
||||
|
||||
171
src/main/main.ts
171
src/main/main.ts
@@ -1,4 +1,4 @@
|
||||
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
||||
import { RealDebridClient } from "./services/download/real-debrid";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
@@ -6,26 +6,18 @@ import { Aria2 } from "./services/aria2";
|
||||
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { Downloader } from "@shared";
|
||||
import {
|
||||
gameAchievementsSublevel,
|
||||
gamesSublevel,
|
||||
levelKeys,
|
||||
db,
|
||||
} from "./level";
|
||||
import { Auth, User, type UserPreferences } from "@types";
|
||||
import { knexClient } from "./knex-client";
|
||||
import { levelKeys, db } from "./level";
|
||||
import type { UserPreferences } from "@types";
|
||||
import { TorBoxClient } from "./services/download/torbox";
|
||||
import { CommonRedistManager } from "./services/common-redist-manager";
|
||||
|
||||
export const loadState = async () => {
|
||||
const userPreferences = await migrateFromSqlite().then(async () => {
|
||||
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
return db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
await import("./events");
|
||||
|
||||
@@ -52,6 +44,15 @@ export const loadState = async () => {
|
||||
return sortBy(games, "timestamp", "DESC");
|
||||
});
|
||||
|
||||
downloads.forEach((download) => {
|
||||
if (download.extracting) {
|
||||
downloadsSublevel.put(levelKeys.game(download.shop, download.objectId), {
|
||||
...download,
|
||||
extracting: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const [nextItemOnQueue] = downloads.filter((game) => game.queued);
|
||||
|
||||
const downloadsToSeed = downloads.filter(
|
||||
@@ -62,141 +63,9 @@ export const loadState = async () => {
|
||||
game.uri !== null
|
||||
);
|
||||
|
||||
console.log("downloadsToSeed", downloadsToSeed);
|
||||
|
||||
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||
|
||||
startMainLoop();
|
||||
};
|
||||
|
||||
const migrateFromSqlite = async () => {
|
||||
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
|
||||
|
||||
if (sqliteMigrationDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const migrateGames = knexClient("game")
|
||||
.select("*")
|
||||
.then((games) => {
|
||||
return gamesSublevel.batch(
|
||||
games.map((game) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(game.shop, game.objectID),
|
||||
value: {
|
||||
objectId: game.objectID,
|
||||
shop: game.shop,
|
||||
title: game.title,
|
||||
iconUrl: game.iconUrl,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
remoteId: game.remoteId,
|
||||
winePrefixPath: game.winePrefixPath,
|
||||
launchOptions: game.launchOptions,
|
||||
executablePath: game.executablePath,
|
||||
isDeleted: game.isDeleted === 1,
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Games migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUserPreferences = knexClient("user_preferences")
|
||||
.select("*")
|
||||
.then(async (userPreferences) => {
|
||||
if (userPreferences.length > 0) {
|
||||
const { realDebridApiToken, ...rest } = userPreferences[0];
|
||||
|
||||
await db.put<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
...rest,
|
||||
realDebridApiToken,
|
||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||
runAtStartup: rest.runAtStartup === 1,
|
||||
startMinimized: rest.startMinimized === 1,
|
||||
disableNsfwAlert: rest.disableNsfwAlert === 1,
|
||||
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
||||
showHiddenAchievementsDescription:
|
||||
rest.showHiddenAchievementsDescription === 1,
|
||||
downloadNotificationsEnabled:
|
||||
rest.downloadNotificationsEnabled === 1,
|
||||
repackUpdatesNotificationsEnabled:
|
||||
rest.repackUpdatesNotificationsEnabled === 1,
|
||||
achievementNotificationsEnabled:
|
||||
rest.achievementNotificationsEnabled === 1,
|
||||
},
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
if (rest.language) {
|
||||
await db.put(levelKeys.language, rest.language);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User preferences migrated successfully");
|
||||
});
|
||||
|
||||
const migrateAchievements = knexClient("game_achievement")
|
||||
.select("*")
|
||||
.then((achievements) => {
|
||||
return gameAchievementsSublevel.batch(
|
||||
achievements.map((achievement) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(achievement.shop, achievement.objectId),
|
||||
value: {
|
||||
achievements: JSON.parse(achievement.achievements),
|
||||
unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Achievements migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUser = knexClient("user_auth")
|
||||
.select("*")
|
||||
.then(async (users) => {
|
||||
if (users.length > 0) {
|
||||
await db.put<string, User>(
|
||||
levelKeys.user,
|
||||
{
|
||||
id: users[0].userId,
|
||||
displayName: users[0].displayName,
|
||||
profileImageUrl: users[0].profileImageUrl,
|
||||
backgroundImageUrl: users[0].backgroundImageUrl,
|
||||
subscription: users[0].subscription,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
await db.put<string, Auth>(
|
||||
levelKeys.auth,
|
||||
{
|
||||
accessToken: users[0].accessToken,
|
||||
refreshToken: users[0].refreshToken,
|
||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User data migrated successfully");
|
||||
});
|
||||
|
||||
return Promise.allSettled([
|
||||
migrateGames,
|
||||
migrateUserPreferences,
|
||||
migrateAchievements,
|
||||
migrateUser,
|
||||
]);
|
||||
|
||||
CommonRedistManager.downloadCommonRedist();
|
||||
};
|
||||
|
||||
74
src/main/services/7zip.ts
Normal file
74
src/main/services/7zip.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { app } from "electron";
|
||||
import cp from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export const binaryName = {
|
||||
linux: "7zzs",
|
||||
darwin: "7zz",
|
||||
win32: "7z.exe",
|
||||
};
|
||||
|
||||
export class SevenZip {
|
||||
private static readonly binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, binaryName[process.platform])
|
||||
: path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"binaries",
|
||||
binaryName[process.platform]
|
||||
);
|
||||
|
||||
public static extractFile(
|
||||
{
|
||||
filePath,
|
||||
outputPath,
|
||||
cwd,
|
||||
passwords = [],
|
||||
}: {
|
||||
filePath: string;
|
||||
outputPath?: string;
|
||||
cwd?: string;
|
||||
passwords?: string[];
|
||||
},
|
||||
successCb: () => void,
|
||||
errorCb: () => void
|
||||
) {
|
||||
const tryPassword = (index = -1) => {
|
||||
const password = passwords[index] ?? "";
|
||||
logger.info(`Trying password ${password} on ${filePath}`);
|
||||
|
||||
const args = ["x", filePath, "-y", "-p" + password];
|
||||
|
||||
if (outputPath) {
|
||||
args.push("-o" + outputPath);
|
||||
}
|
||||
|
||||
const child = cp.execFile(this.binaryPath, args, {
|
||||
cwd,
|
||||
});
|
||||
|
||||
child.once("exit", (code) => {
|
||||
if (code === 0) {
|
||||
successCb();
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < passwords.length - 1) {
|
||||
logger.info(
|
||||
`Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
|
||||
);
|
||||
|
||||
tryPassword(index + 1);
|
||||
} else {
|
||||
logger.info(`Failed to extract file: ${filePath}`);
|
||||
|
||||
errorCb();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryPassword();
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,15 @@ import path from "node:path";
|
||||
import cp from "node:child_process";
|
||||
import { app } from "electron";
|
||||
|
||||
export const startAria2 = () => {};
|
||||
|
||||
export class Aria2 {
|
||||
private static process: cp.ChildProcess | null = null;
|
||||
private static readonly binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
|
||||
public static spawn() {
|
||||
const binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
|
||||
this.process = cp.spawn(
|
||||
binaryPath,
|
||||
this.binaryPath,
|
||||
[
|
||||
"--enable-rpc",
|
||||
"--rpc-listen-all",
|
||||
|
||||
111
src/main/services/cloud-sync.ts
Normal file
111
src/main/services/cloud-sync.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { levelKeys, gamesSublevel, db } from "@main/level";
|
||||
import { app } from "electron";
|
||||
import path from "node:path";
|
||||
import * as tar from "tar";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import type { GameShop, User } from "@types";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
import { Ludusavi } from "./ludusavi";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
|
||||
export class CloudSync {
|
||||
private static async bundleBackup(
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix: string | null
|
||||
) {
|
||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||
|
||||
// Remove existing backup
|
||||
if (fs.existsSync(backupPath)) {
|
||||
fs.rmSync(backupPath, { recursive: true });
|
||||
}
|
||||
|
||||
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||
|
||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||
|
||||
await tar.create(
|
||||
{
|
||||
gzip: false,
|
||||
file: tarLocation,
|
||||
cwd: backupPath,
|
||||
},
|
||||
["."]
|
||||
);
|
||||
|
||||
return tarLocation;
|
||||
}
|
||||
|
||||
public static async uploadSaveGame(
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null,
|
||||
label?: string
|
||||
) {
|
||||
const hasActiveSubscription = await db
|
||||
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
|
||||
.then((user) => {
|
||||
const expiresAt = new Date(user?.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
});
|
||||
|
||||
if (!hasActiveSubscription) {
|
||||
throw new SubscriptionRequiredError();
|
||||
}
|
||||
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const bundleLocation = await this.bundleBackup(
|
||||
shop,
|
||||
objectId,
|
||||
game?.winePrefixPath ?? null
|
||||
);
|
||||
|
||||
const stat = await fs.promises.stat(bundleLocation);
|
||||
|
||||
const { uploadUrl } = await HydraApi.post<{
|
||||
id: string;
|
||||
uploadUrl: string;
|
||||
}>("/profile/games/artifacts", {
|
||||
artifactLengthInBytes: stat.size,
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(app.getPath("home")),
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
label,
|
||||
});
|
||||
|
||||
const fileBuffer = await fs.promises.readFile(bundleLocation);
|
||||
|
||||
await axios.put(uploadUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/tar",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
logger.log(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-upload-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(bundleLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to remove tar file", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
109
src/main/services/common-redist-manager.ts
Normal file
109
src/main/services/common-redist-manager.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { commonRedistPath } from "@main/constants";
|
||||
import axios from "axios";
|
||||
import fs from "node:fs";
|
||||
import cp from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { logger } from "./logger";
|
||||
import { app } from "electron";
|
||||
import { WindowManager } from "./window-manager";
|
||||
|
||||
export class CommonRedistManager {
|
||||
private static readonly redistributables = [
|
||||
"dotNetFx40_Full_setup.exe",
|
||||
"dxwebsetup.exe",
|
||||
"oalinst.exe",
|
||||
"install.bat",
|
||||
"vcredist_2015-2019_x64.exe",
|
||||
"vcredist_2015-2019_x86.exe",
|
||||
"vcredist_x64.exe",
|
||||
"vcredist_x86.exe",
|
||||
"xnafx40_redist.msi",
|
||||
];
|
||||
private static readonly installationTimeout = 1000 * 60 * 5; // 5 minutes
|
||||
private static readonly installationLog = path.join(
|
||||
app.getPath("temp"),
|
||||
"common_redist_install.log"
|
||||
);
|
||||
|
||||
public static async installCommonRedist() {
|
||||
const abortController = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
abortController.abort();
|
||||
logger.error("Installation timed out");
|
||||
|
||||
WindowManager.mainWindow?.webContents.send("common-redist-progress", {
|
||||
log: "Installation timed out",
|
||||
complete: false,
|
||||
});
|
||||
}, this.installationTimeout);
|
||||
|
||||
const installationCompleteMessage = "Installation complete";
|
||||
|
||||
if (!fs.existsSync(this.installationLog)) {
|
||||
await fs.promises.writeFile(this.installationLog, "");
|
||||
}
|
||||
|
||||
fs.watch(this.installationLog, { signal: abortController.signal }, () => {
|
||||
fs.readFile(this.installationLog, "utf-8", (err, data) => {
|
||||
if (err) return logger.error("Error reading log file:", err);
|
||||
|
||||
const tail = data.split("\n").at(-2)?.trim();
|
||||
|
||||
if (tail?.includes(installationCompleteMessage)) {
|
||||
clearTimeout(timeout);
|
||||
if (!abortController.signal.aborted) {
|
||||
abortController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.webContents.send("common-redist-progress", {
|
||||
log: tail,
|
||||
complete: tail?.includes(installationCompleteMessage),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cp.exec(
|
||||
path.join(commonRedistPath, "install.bat"),
|
||||
{
|
||||
windowsHide: true,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
logger.error("Failed to run install.bat", error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static async canInstallCommonRedist() {
|
||||
return this.redistributables.every((redist) => {
|
||||
const filePath = path.join(commonRedistPath, redist);
|
||||
|
||||
return fs.existsSync(filePath);
|
||||
});
|
||||
}
|
||||
|
||||
public static async downloadCommonRedist() {
|
||||
if (!fs.existsSync(commonRedistPath)) {
|
||||
await fs.promises.mkdir(commonRedistPath, { recursive: true });
|
||||
}
|
||||
|
||||
for (const redist of this.redistributables) {
|
||||
const filePath = path.join(commonRedistPath, redist);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await axios.get(
|
||||
`https://github.com/hydralauncher/hydra-common-redist/raw/refs/heads/main/${redist}`,
|
||||
{
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
|
||||
await fs.promises.writeFile(filePath, response.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Downloader, DownloadError } from "@shared";
|
||||
import { Downloader, DownloadError, FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
|
||||
import {
|
||||
GofileApi,
|
||||
QiwiApi,
|
||||
DatanodesApi,
|
||||
MediafireApi,
|
||||
PixelDrainApi,
|
||||
} from "../hosters";
|
||||
import { PythonRPC } from "../python-rpc";
|
||||
import {
|
||||
LibtorrentPayload,
|
||||
@@ -16,6 +22,7 @@ import { logger } from "../logger";
|
||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { TorBoxClient } from "./torbox";
|
||||
import { GameFilesManager } from "../game-files-manager";
|
||||
|
||||
export class DownloadManager {
|
||||
private static downloadingGameId: string | null = null;
|
||||
@@ -130,6 +137,8 @@ export class DownloadManager {
|
||||
);
|
||||
}
|
||||
|
||||
const shouldExtract = download.automaticallyExtract;
|
||||
|
||||
if (progress === 1 && download) {
|
||||
publishDownloadCompleteNotification(game);
|
||||
|
||||
@@ -137,23 +146,48 @@ export class DownloadManager {
|
||||
userPreferences?.seedAfterDownloadComplete &&
|
||||
download.downloader === Downloader.Torrent
|
||||
) {
|
||||
downloadsSublevel.put(gameId, {
|
||||
await downloadsSublevel.put(gameId, {
|
||||
...download,
|
||||
status: "seeding",
|
||||
shouldSeed: true,
|
||||
queued: false,
|
||||
extracting: shouldExtract,
|
||||
});
|
||||
} else {
|
||||
downloadsSublevel.put(gameId, {
|
||||
await downloadsSublevel.put(gameId, {
|
||||
...download,
|
||||
status: "complete",
|
||||
shouldSeed: false,
|
||||
queued: false,
|
||||
extracting: shouldExtract,
|
||||
});
|
||||
|
||||
this.cancelDownload(gameId);
|
||||
}
|
||||
|
||||
if (shouldExtract) {
|
||||
const gameFilesManager = new GameFilesManager(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
|
||||
if (
|
||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
|
||||
download.folderName?.endsWith(ext)
|
||||
)
|
||||
) {
|
||||
gameFilesManager.extractDownloadedFile();
|
||||
} else {
|
||||
gameFilesManager
|
||||
.extractFilesInDirectory(
|
||||
path.join(download.downloadPath, download.folderName!)
|
||||
)
|
||||
.then(() => {
|
||||
gameFilesManager.setExtractionComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const downloads = await downloadsSublevel
|
||||
.values()
|
||||
.all()
|
||||
@@ -283,11 +317,12 @@ export class DownloadManager {
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = download.uri.split("/").pop();
|
||||
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: downloadId,
|
||||
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
|
||||
url: downloadUrl,
|
||||
save_path: download.downloadPath,
|
||||
};
|
||||
}
|
||||
@@ -336,6 +371,7 @@ export class DownloadManager {
|
||||
game_id: downloadId,
|
||||
url: downloadUrl,
|
||||
save_path: download.downloadPath,
|
||||
allow_multiple_connections: true,
|
||||
};
|
||||
}
|
||||
case Downloader.TorBox: {
|
||||
@@ -348,6 +384,7 @@ export class DownloadManager {
|
||||
url,
|
||||
save_path: download.downloadPath,
|
||||
out: name,
|
||||
allow_multiple_connections: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
TorBoxAddTorrentRequest,
|
||||
TorBoxRequestLinkRequest,
|
||||
} from "@types";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
export class TorBoxClient {
|
||||
private static instance: AxiosInstance;
|
||||
@@ -18,6 +19,7 @@ export class TorBoxClient {
|
||||
baseURL: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"User-Agent": `Hydra/${appVersion}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
158
src/main/services/game-files-manager.ts
Normal file
158
src/main/services/game-files-manager.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { GameShop } from "@types";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
|
||||
import { SevenZip } from "./7zip";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { publishExtractionCompleteNotification } from "./notifications";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export class GameFilesManager {
|
||||
constructor(
|
||||
private readonly shop: GameShop,
|
||||
private readonly objectId: string
|
||||
) {}
|
||||
|
||||
private async clearExtractionState() {
|
||||
const gameKey = levelKeys.game(this.shop, this.objectId);
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download!,
|
||||
extracting: false,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-extraction-complete",
|
||||
this.shop,
|
||||
this.objectId
|
||||
);
|
||||
}
|
||||
|
||||
async extractFilesInDirectory(directoryPath: string) {
|
||||
if (!fs.existsSync(directoryPath)) return;
|
||||
const files = await fs.promises.readdir(directoryPath);
|
||||
|
||||
const compressedFiles = files.filter((file) =>
|
||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) => file.endsWith(ext))
|
||||
);
|
||||
|
||||
const filesToExtract = compressedFiles.filter(
|
||||
(file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
filesToExtract.map((file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
SevenZip.extractFile(
|
||||
{
|
||||
filePath: path.join(directoryPath, file),
|
||||
cwd: directoryPath,
|
||||
passwords: ["online-fix.me", "steamrip.com"],
|
||||
},
|
||||
() => {
|
||||
resolve(true);
|
||||
},
|
||||
() => {
|
||||
reject(new Error(`Failed to extract file: ${file}`));
|
||||
this.clearExtractionState();
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
compressedFiles.forEach((file) => {
|
||||
const extractionPath = path.join(directoryPath, file);
|
||||
|
||||
if (fs.existsSync(extractionPath)) {
|
||||
fs.unlink(extractionPath, (err) => {
|
||||
if (err) {
|
||||
logger.error(`Failed to delete file: ${file}`, err);
|
||||
|
||||
this.clearExtractionState();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setExtractionComplete(publishNotification = true) {
|
||||
const gameKey = levelKeys.game(this.shop, this.objectId);
|
||||
|
||||
const [download, game] = await Promise.all([
|
||||
downloadsSublevel.get(gameKey),
|
||||
gamesSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download!,
|
||||
extracting: false,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-extraction-complete",
|
||||
this.shop,
|
||||
this.objectId
|
||||
);
|
||||
|
||||
if (publishNotification) {
|
||||
publishExtractionCompleteNotification(game!);
|
||||
}
|
||||
}
|
||||
|
||||
async extractDownloadedFile() {
|
||||
const gameKey = levelKeys.game(this.shop, this.objectId);
|
||||
|
||||
const [download, game] = await Promise.all([
|
||||
downloadsSublevel.get(gameKey),
|
||||
gamesSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
if (!download || !game) return false;
|
||||
|
||||
const filePath = path.join(download.downloadPath, download.folderName!);
|
||||
|
||||
const extractionPath = path.join(
|
||||
download.downloadPath,
|
||||
path.parse(download.folderName!).name
|
||||
);
|
||||
|
||||
SevenZip.extractFile(
|
||||
{
|
||||
filePath,
|
||||
outputPath: extractionPath,
|
||||
passwords: ["online-fix.me", "steamrip.com"],
|
||||
},
|
||||
async () => {
|
||||
await this.extractFilesInDirectory(extractionPath);
|
||||
|
||||
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
logger.error(
|
||||
`Failed to delete file: ${download.folderName}`,
|
||||
err
|
||||
);
|
||||
|
||||
this.clearExtractionState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download!,
|
||||
folderName: path.parse(download.folderName!).name,
|
||||
});
|
||||
|
||||
this.setExtractionComplete();
|
||||
},
|
||||
() => {
|
||||
this.clearExtractionState();
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,71 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { wrapper } from "axios-cookiejar-support";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
|
||||
export class DatanodesApi {
|
||||
private static readonly session = axios.create({});
|
||||
private static readonly jar = new CookieJar();
|
||||
|
||||
private static readonly session = wrapper(
|
||||
axios.create({
|
||||
jar: DatanodesApi.jar,
|
||||
withCredentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/");
|
||||
try {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
|
||||
const fileCode = pathSegments[0];
|
||||
|
||||
const fileCode = decodeURIComponent(pathSegments[1]);
|
||||
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
|
||||
await this.jar.setCookie("lang=english;", "https://datanodes.to");
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
rand: "",
|
||||
referer: "https://datanodes.to/download",
|
||||
method_free: "Free Download >>",
|
||||
method_premium: "",
|
||||
adblock_detected: "",
|
||||
});
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
method_free: "Free Download >>",
|
||||
dl: "1",
|
||||
});
|
||||
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
|
||||
Host: "datanodes.to",
|
||||
Origin: "https://datanodes.to",
|
||||
Referer: "https://datanodes.to/download",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||
Referer: "https://datanodes.to/download",
|
||||
Origin: "https://datanodes.to",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
if (typeof response.data === "object" && response.data.url) {
|
||||
return decodeURIComponent(response.data.url);
|
||||
}
|
||||
|
||||
const htmlContent = String(response.data);
|
||||
if (!htmlContent) {
|
||||
throw new Error("Empty response received");
|
||||
}
|
||||
|
||||
const downloadLinkRegex = /href=["'](https:\/\/[^"']+)["']/;
|
||||
const downloadLinkMatch = downloadLinkRegex.exec(htmlContent);
|
||||
if (downloadLinkMatch) {
|
||||
return downloadLinkMatch[1];
|
||||
}
|
||||
|
||||
throw new Error("Failed to get the download link");
|
||||
} catch (error) {
|
||||
console.error("Error fetching download URL:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./gofile";
|
||||
export * from "./qiwi";
|
||||
export * from "./datanodes";
|
||||
export * from "./mediafire";
|
||||
export * from "./pixeldrain";
|
||||
|
||||
42
src/main/services/hosters/pixeldrain.ts
Normal file
42
src/main/services/hosters/pixeldrain.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import axios from "axios";
|
||||
|
||||
export class PixelDrainApi {
|
||||
private static readonly browserHeaders = {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
DNT: "1",
|
||||
Connection: "keep-alive",
|
||||
"Sec-Fetch-Dest": "document",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "none",
|
||||
"Sec-Fetch-User": "?1",
|
||||
};
|
||||
|
||||
public static async getDownloadUrl(fileId: string): Promise<string> {
|
||||
try {
|
||||
const response = await axios.get(`https://pd.cybar.xyz/${fileId}`, {
|
||||
headers: this.browserHeaders,
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) =>
|
||||
status === 301 || status === 302 || status === 200,
|
||||
});
|
||||
|
||||
if (
|
||||
response.headers.location ||
|
||||
response.status === 301 ||
|
||||
response.status === 302
|
||||
) {
|
||||
return response.headers.location;
|
||||
}
|
||||
|
||||
throw new Error(`No redirect URL found (status: ${response.status})`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching PixelDrain URL:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
||||
import { omit } from "lodash-es";
|
||||
import { appVersion } from "@main/constants";
|
||||
import { getUserData } from "./user/get-user-data";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
import type { Auth, User } from "@types";
|
||||
@@ -47,8 +46,8 @@ export class HydraApi {
|
||||
}
|
||||
|
||||
private static hasActiveSubscription() {
|
||||
const expiresAt = this.userAuth.subscription?.expiresAt;
|
||||
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
|
||||
const expiresAt = new Date(this.userAuth.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
}
|
||||
|
||||
static async handleExternalAuth(uri: string) {
|
||||
|
||||
@@ -7,3 +7,7 @@ export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
export * from "./hydra-api";
|
||||
export * from "./ludusavi";
|
||||
export * from "./cloud-sync";
|
||||
export * from "./7zip";
|
||||
export * from "./game-files-manager";
|
||||
export * from "./common-redist-manager";
|
||||
|
||||
@@ -3,18 +3,21 @@ import { DownloadManager } from "./download";
|
||||
import { watchProcesses } from "./process-watcher";
|
||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||
import { UpdateManager } from "./update-manager";
|
||||
import { watchFriendRequests } from "@main/events/profile/sync-friend-requests";
|
||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||
|
||||
export const startMainLoop = async () => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await Promise.allSettled([
|
||||
watchProcesses(),
|
||||
watchFriendRequests(),
|
||||
DownloadManager.watchDownloads(),
|
||||
AchievementWatcherManager.watchAchievements(),
|
||||
DownloadManager.getSeedStatus(),
|
||||
UpdateManager.checkForUpdatePeriodically(),
|
||||
]);
|
||||
|
||||
await sleep(1500);
|
||||
await sleep(MAIN_LOOP_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { logger } from "../logger";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import type { Game, UserPreferences } from "@types";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
|
||||
|
||||
async function downloadImage(url: string | null) {
|
||||
if (!url) return undefined;
|
||||
@@ -72,10 +73,33 @@ export const publishNotificationUpdateReadyToInstall = async (
|
||||
ns: "notifications",
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
})
|
||||
.on("click", () => {
|
||||
restartAndInstallUpdate();
|
||||
})
|
||||
.show();
|
||||
};
|
||||
|
||||
export const publishNewFriendRequestNotification = async () => {};
|
||||
export const publishNewFriendRequestNotification = async () => {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (!userPreferences?.friendRequestNotificationsEnabled) return;
|
||||
|
||||
new Notification({
|
||||
title: t("new_friend_request_title", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
body: t("new_friend_request_description", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
};
|
||||
|
||||
export const publishCombinedNewAchievementNotification = async (
|
||||
achievementCount,
|
||||
@@ -104,6 +128,17 @@ export const publishCombinedNewAchievementNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const publishExtractionCompleteNotification = async (game: Game) => {
|
||||
new Notification({
|
||||
title: t("extraction_complete", { ns: "notifications" }),
|
||||
body: t("game_extracted", {
|
||||
ns: "notifications",
|
||||
title: game.title,
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
};
|
||||
|
||||
export const publishNewAchievementNotification = async (info: {
|
||||
achievements: { displayName: string; iconUrl: string }[];
|
||||
unlockedAchievementCount: number;
|
||||
|
||||
@@ -6,6 +6,9 @@ import axios from "axios";
|
||||
import { exec } from "child_process";
|
||||
import { ProcessPayload } from "./download/types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import i18next, { t } from "i18next";
|
||||
import { CloudSync } from "./cloud-sync";
|
||||
import { formatDate } from "date-fns";
|
||||
|
||||
const commands = {
|
||||
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
|
||||
@@ -225,6 +228,20 @@ function onOpenGame(game: Game) {
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
||||
|
||||
const { language } = i18next;
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
game.objectId,
|
||||
game.shop,
|
||||
null,
|
||||
t("automatic_backup_from", {
|
||||
ns: "game_details",
|
||||
date: formatDate(new Date(), language),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
||||
}
|
||||
@@ -281,12 +298,26 @@ const onCloseGame = (game: Game) => {
|
||||
)!;
|
||||
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
|
||||
|
||||
const { language } = i18next;
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
performance.now() - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
).catch(() => {});
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
game.objectId,
|
||||
game.shop,
|
||||
null,
|
||||
t("automatic_backup_from", {
|
||||
ns: "game_details",
|
||||
date: formatDate(new Date(), language),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
createGame(game).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import updater, { UpdateInfo } from "electron-updater";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import { AppUpdaterEvent } from "@types";
|
||||
import { AppUpdaterEvent, UserPreferences } from "@types";
|
||||
import { app } from "electron";
|
||||
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
||||
|
||||
const isAutoInstallAvailable =
|
||||
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||
|
||||
const { autoUpdater } = updater;
|
||||
const sendEventsForDebug = false;
|
||||
const ticksToUpdate = (50 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 50 minutes
|
||||
|
||||
export class UpdateManager {
|
||||
private static hasNotified = false;
|
||||
@@ -16,7 +16,7 @@ export class UpdateManager {
|
||||
private static checkTick = 0;
|
||||
|
||||
private static mockValuesForDebug() {
|
||||
this.sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
||||
this.sendEvent({ type: "update-available", info: { version: "3.3.1" } });
|
||||
this.sendEvent({ type: "update-downloaded" });
|
||||
}
|
||||
|
||||
@@ -24,7 +24,27 @@ export class UpdateManager {
|
||||
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
||||
}
|
||||
|
||||
public static checkForUpdates() {
|
||||
private static async isAutoInstallEnabled() {
|
||||
if (process.platform === "darwin") return false;
|
||||
if (process.platform === "win32") {
|
||||
return process.env.PORTABLE_EXECUTABLE_FILE == null;
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
return userPreferences?.enableAutoInstall === true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async checkForUpdates() {
|
||||
autoUpdater
|
||||
.once("update-available", (info: UpdateInfo) => {
|
||||
this.sendEvent({ type: "update-available", info });
|
||||
@@ -39,6 +59,8 @@ export class UpdateManager {
|
||||
}
|
||||
});
|
||||
|
||||
const isAutoInstallAvailable = await this.isAutoInstallEnabled();
|
||||
|
||||
if (app.isPackaged) {
|
||||
autoUpdater.autoDownload = isAutoInstallAvailable;
|
||||
autoUpdater.checkForUpdates().then((result) => {
|
||||
@@ -52,7 +74,7 @@ export class UpdateManager {
|
||||
}
|
||||
|
||||
public static checkForUpdatePeriodically() {
|
||||
if (this.checkTick % 2000 == 0) {
|
||||
if (this.checkTick % ticksToUpdate == 0) {
|
||||
this.checkForUpdates();
|
||||
}
|
||||
this.checkTick++;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { HydraApi } from "./hydra-api";
|
||||
import UserAgent from "user-agents";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { slice, sortBy } from "lodash-es";
|
||||
import type { UserPreferences } from "@types";
|
||||
import type { ScreenState, UserPreferences } from "@types";
|
||||
import { AuthPage } from "@shared";
|
||||
import { isStaging } from "@main/constants";
|
||||
|
||||
@@ -26,27 +26,8 @@ export class WindowManager {
|
||||
|
||||
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||
|
||||
private static loadMainWindowURL(hash = "") {
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.mainWindow?.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`
|
||||
);
|
||||
} else {
|
||||
this.mainWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static createMainWindow() {
|
||||
if (this.mainWindow) return;
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions =
|
||||
{
|
||||
width: 1200,
|
||||
height: 720,
|
||||
minWidth: 1024,
|
||||
@@ -65,7 +46,65 @@ export class WindowManager {
|
||||
sandbox: false,
|
||||
},
|
||||
show: false,
|
||||
};
|
||||
|
||||
private static loadMainWindowURL(hash = "") {
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.mainWindow?.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`
|
||||
);
|
||||
} else {
|
||||
this.mainWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async saveScreenConfig(configScreenWhenClosed: ScreenState) {
|
||||
await db.put(levelKeys.screenState, configScreenWhenClosed, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
}
|
||||
|
||||
private static async loadScreenConfig() {
|
||||
const data = await db.get<string, ScreenState | undefined>(
|
||||
levelKeys.screenState,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
return data ?? { isMaximized: false, height: 720, width: 1200 };
|
||||
}
|
||||
|
||||
private static updateInitialConfig(
|
||||
newConfig: Partial<Electron.BrowserWindowConstructorOptions>
|
||||
) {
|
||||
this.initialConfigInitializationMainWindow = {
|
||||
...this.initialConfigInitializationMainWindow,
|
||||
...newConfig,
|
||||
};
|
||||
}
|
||||
|
||||
public static async createMainWindow() {
|
||||
if (this.mainWindow) return;
|
||||
|
||||
const { isMaximized = false, ...configWithoutMaximized } =
|
||||
await this.loadScreenConfig();
|
||||
|
||||
this.updateInitialConfig(configWithoutMaximized);
|
||||
|
||||
this.mainWindow = new BrowserWindow(
|
||||
this.initialConfigInitializationMainWindow
|
||||
);
|
||||
|
||||
if (isMaximized) {
|
||||
this.mainWindow.maximize();
|
||||
}
|
||||
|
||||
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
(details, callback) => {
|
||||
@@ -143,9 +182,26 @@ export class WindowManager {
|
||||
}
|
||||
);
|
||||
|
||||
if (this.mainWindow) {
|
||||
const lastBounds = this.mainWindow.getBounds();
|
||||
const isMaximized = this.mainWindow.isMaximized() ?? false;
|
||||
const screenConfig = isMaximized
|
||||
? {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
height: this.initialConfigInitializationMainWindow.height ?? 720,
|
||||
width: this.initialConfigInitializationMainWindow.width ?? 1200,
|
||||
isMaximized: true,
|
||||
}
|
||||
: { ...lastBounds, isMaximized };
|
||||
|
||||
await this.saveScreenConfig(screenConfig);
|
||||
}
|
||||
|
||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
SeedingStatus,
|
||||
GameAchievement,
|
||||
Theme,
|
||||
FriendRequestSync,
|
||||
} from "@types";
|
||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
@@ -99,8 +100,24 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
/* Download sources */
|
||||
putDownloadSource: (objectIds: string[]) =>
|
||||
ipcRenderer.invoke("putDownloadSource", objectIds),
|
||||
createDownloadSources: (urls: string[]) =>
|
||||
ipcRenderer.invoke("createDownloadSources", urls),
|
||||
removeDownloadSource: (url: string, removeAll?: boolean) =>
|
||||
ipcRenderer.invoke("removeDownloadSource", url, removeAll),
|
||||
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||
|
||||
/* Library */
|
||||
toggleAutomaticCloudSync: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
automaticCloudSync: boolean
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"toggleAutomaticCloudSync",
|
||||
shop,
|
||||
objectId,
|
||||
automaticCloudSync
|
||||
),
|
||||
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
||||
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
||||
createGameShortcut: (shop: GameShop, objectId: string) =>
|
||||
@@ -161,6 +178,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
@@ -183,6 +202,15 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
||||
},
|
||||
onExtractionComplete: (cb: (shop: GameShop, objectId: string) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => cb(shop, objectId);
|
||||
ipcRenderer.on("on-extraction-complete", listener);
|
||||
return () => ipcRenderer.removeListener("on-extraction-complete", listener);
|
||||
},
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) =>
|
||||
@@ -266,6 +294,9 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
showItemInFolder: (path: string) =>
|
||||
ipcRenderer.invoke("showItemInFolder", path),
|
||||
getFeatures: () => ipcRenderer.invoke("getFeatures"),
|
||||
getBadges: () => ipcRenderer.invoke("getBadges"),
|
||||
canInstallCommonRedist: () => ipcRenderer.invoke("canInstallCommonRedist"),
|
||||
installCommonRedist: () => ipcRenderer.invoke("installCommonRedist"),
|
||||
platform: process.platform,
|
||||
|
||||
/* Auto update */
|
||||
@@ -281,6 +312,16 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.removeListener("autoUpdaterEvent", listener);
|
||||
};
|
||||
},
|
||||
onCommonRedistProgress: (
|
||||
cb: (value: { log: string; complete: boolean }) => void
|
||||
) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
value: { log: string; complete: boolean }
|
||||
) => cb(value);
|
||||
ipcRenderer.on("common-redist-progress", listener);
|
||||
return () => ipcRenderer.removeListener("common-redist-progress", listener);
|
||||
},
|
||||
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
|
||||
restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"),
|
||||
|
||||
@@ -294,6 +335,15 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
|
||||
onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
friendRequests: FriendRequestSync
|
||||
) => cb(friendRequests);
|
||||
ipcRenderer.on("on-sync-friend-requests", listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-sync-friend-requests", listener);
|
||||
},
|
||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||
sendFriendRequest: (userId: string) =>
|
||||
@@ -325,6 +375,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
||||
|
||||
/* Auth */
|
||||
getAuth: () => ipcRenderer.invoke("getAuth"),
|
||||
signOut: () => ipcRenderer.invoke("signOut"),
|
||||
openAuthWindow: (page: AuthPage) =>
|
||||
ipcRenderer.invoke("openAuthWindow", page),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
setUserDetails,
|
||||
setProfileBackground,
|
||||
setGameRunning,
|
||||
setFriendRequestCount,
|
||||
} from "@renderer/features";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||
@@ -30,6 +31,7 @@ import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-m
|
||||
|
||||
import { injectCustomCss } from "./helpers";
|
||||
import "./app.scss";
|
||||
import { DownloadSource } from "@types";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
@@ -51,7 +53,6 @@ export function App() {
|
||||
isFriendsModalVisible,
|
||||
friendRequetsModalTab,
|
||||
friendModalUserId,
|
||||
syncFriendRequests,
|
||||
hideFriendsModal,
|
||||
fetchUserDetails,
|
||||
updateUserDetails,
|
||||
@@ -123,7 +124,7 @@ export function App() {
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
syncFriendRequests();
|
||||
window.electron.syncFriendRequests();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -134,24 +135,100 @@ export function App() {
|
||||
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`;
|
||||
document.head.appendChild($script);
|
||||
});
|
||||
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
|
||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
||||
|
||||
const syncDownloadSources = useCallback(async () => {
|
||||
const downloadSources = await window.electron.getDownloadSources();
|
||||
|
||||
const existingDownloadSources: DownloadSource[] =
|
||||
await downloadSourcesTable.toArray();
|
||||
|
||||
window.electron.createDownloadSources(
|
||||
existingDownloadSources.map((source) => source.url)
|
||||
);
|
||||
|
||||
await Promise.allSettled(
|
||||
downloadSources.map(async (source) => {
|
||||
return new Promise((resolve) => {
|
||||
const existingDownloadSource = existingDownloadSources.find(
|
||||
(downloadSource) => downloadSource.url === source.url
|
||||
);
|
||||
|
||||
if (!existingDownloadSource) {
|
||||
const channel = new BroadcastChannel(
|
||||
`download_sources:import:${source.url}`
|
||||
);
|
||||
|
||||
downloadSourcesWorker.postMessage([
|
||||
"IMPORT_DOWNLOAD_SOURCE",
|
||||
source.url,
|
||||
]);
|
||||
|
||||
channel.onmessage = () => {
|
||||
resolve(true);
|
||||
channel.close();
|
||||
};
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
updateRepacks();
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||
|
||||
channel.onmessage = async (event: MessageEvent<number>) => {
|
||||
const newRepacksCount = event.data;
|
||||
window.electron.publishNewRepacksNotification(newRepacksCount);
|
||||
updateRepacks();
|
||||
|
||||
const downloadSources = await downloadSourcesTable.toArray();
|
||||
|
||||
downloadSources
|
||||
.filter((source) => !source.fingerprint)
|
||||
.forEach(async (downloadSource) => {
|
||||
const { fingerprint } = await window.electron.putDownloadSource(
|
||||
downloadSource.objectIds
|
||||
);
|
||||
|
||||
downloadSourcesTable.update(downloadSource.id, { fingerprint });
|
||||
});
|
||||
};
|
||||
|
||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||
}, [updateRepacks]);
|
||||
|
||||
const onSignIn = useCallback(() => {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
syncFriendRequests();
|
||||
window.electron.syncFriendRequests();
|
||||
showSuccessToast(t("successfully_signed_in"));
|
||||
}
|
||||
});
|
||||
|
||||
syncDownloadSources();
|
||||
}, [
|
||||
fetchUserDetails,
|
||||
syncFriendRequests,
|
||||
t,
|
||||
showSuccessToast,
|
||||
updateUserDetails,
|
||||
syncDownloadSources,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
|
||||
dispatch(setFriendRequestCount(result.friendRequestCount));
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
||||
if (gamesRunning.length) {
|
||||
@@ -208,31 +285,8 @@ export function App() {
|
||||
}, [dispatch, draggingDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
updateRepacks();
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||
|
||||
channel.onmessage = async (event: MessageEvent<number>) => {
|
||||
const newRepacksCount = event.data;
|
||||
window.electron.publishNewRepacksNotification(newRepacksCount);
|
||||
updateRepacks();
|
||||
|
||||
const downloadSources = await downloadSourcesTable.toArray();
|
||||
|
||||
downloadSources
|
||||
.filter((source) => !source.fingerprint)
|
||||
.forEach(async (downloadSource) => {
|
||||
const { fingerprint } = await window.electron.putDownloadSource(
|
||||
downloadSource.objectIds
|
||||
);
|
||||
|
||||
downloadSourcesTable.update(downloadSource.id, { fingerprint });
|
||||
});
|
||||
};
|
||||
|
||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||
}, [updateRepacks]);
|
||||
syncDownloadSources();
|
||||
}, [syncDownloadSources]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAndApplyTheme = async () => {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<svg width="240" height="246" viewBox="0 0 240 246" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint0_linear_1378_2496)"/>
|
||||
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint1_linear_1378_2496)"/>
|
||||
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" stroke="url(#paint2_linear_1378_2496)"/>
|
||||
<g opacity="0.9">
|
||||
<g style="mix-blend-mode:overlay">
|
||||
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" fill="black"/>
|
||||
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" stroke="url(#paint3_linear_1378_2496)" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1378_2496" x1="5.63736e-07" y1="12.92" x2="246" y2="233.08" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0CF1CA"/>
|
||||
<stop offset="1" stop-color="#1DCCEB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1378_2496" x1="19.8951" y1="-3.50306e-06" x2="226.105" y2="246" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0DDEBB"/>
|
||||
<stop offset="1" stop-color="#052520"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1378_2496" x1="-1.9947e-06" y1="18.0561" x2="246" y2="227.944" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.7"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1378_2496" x1="61.9253" y1="71.6411" x2="164.664" y2="169.814" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -10,6 +10,7 @@
|
||||
cursor: pointer;
|
||||
color: globals.$muted-color;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&__image {
|
||||
height: 100%;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PersonIcon } from "@primer/octicons-react";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./avatar.scss";
|
||||
|
||||
@@ -14,11 +15,18 @@ export interface AvatarProps
|
||||
src?: string | null;
|
||||
}
|
||||
|
||||
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
|
||||
export function Avatar({ size, alt, src, className, ...props }: AvatarProps) {
|
||||
return (
|
||||
<div className="profile-avatar" style={{ width: size, height: size }}>
|
||||
{src ? (
|
||||
<img className="profile-avatar__image" alt={alt} src={src} {...props} />
|
||||
<img
|
||||
className={cn("profile-avatar__image", className)}
|
||||
alt={alt}
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={size * 0.7} />
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDownload, useLibrary, useUserDetails } from "@renderer/hooks";
|
||||
import {
|
||||
useDownload,
|
||||
useLibrary,
|
||||
useToast,
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
|
||||
import "./bottom-panel.scss";
|
||||
|
||||
@@ -17,20 +22,52 @@ export function BottomPanel() {
|
||||
|
||||
const { library } = useLibrary();
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||
|
||||
const [version, setVersion] = useState("");
|
||||
const [sessionHash, setSessionHash] = useState<null | string>("");
|
||||
const [commonRedistStatus, setCommonRedistStatus] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getVersion().then((result) => setVersion(result));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = window.electron.onCommonRedistProgress(
|
||||
({ log, complete }) => {
|
||||
if (log === "Installation timed out" || complete) {
|
||||
setCommonRedistStatus(null);
|
||||
|
||||
if (complete) {
|
||||
showSuccessToast(
|
||||
t("installation_complete"),
|
||||
t("installation_complete_message")
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setCommonRedistStatus(log);
|
||||
}
|
||||
);
|
||||
|
||||
return () => unlisten();
|
||||
}, [t, showSuccessToast]);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getSessionHash().then((result) => setSessionHash(result));
|
||||
}, [userDetails?.id]);
|
||||
|
||||
const status = useMemo(() => {
|
||||
if (commonRedistStatus) {
|
||||
return t("installing_common_redist", { log: commonRedistStatus });
|
||||
}
|
||||
|
||||
const game = lastPacket
|
||||
? library.find((game) => game.id === lastPacket?.gameId)
|
||||
: undefined;
|
||||
@@ -64,7 +101,15 @@ export function BottomPanel() {
|
||||
}
|
||||
|
||||
return t("no_downloads_in_progress");
|
||||
}, [t, library, lastPacket, progress, eta, downloadSpeed]);
|
||||
}, [
|
||||
t,
|
||||
library,
|
||||
lastPacket,
|
||||
progress,
|
||||
eta,
|
||||
downloadSpeed,
|
||||
commonRedistStatus,
|
||||
]);
|
||||
|
||||
return (
|
||||
<footer className="bottom-panel">
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface CheckboxFieldProps
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> {
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
}
|
||||
|
||||
export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
|
||||
|
||||
onConfirm: () => void;
|
||||
onCancel?: () => void;
|
||||
|
||||
buttonsIsDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function ConfirmationModal({
|
||||
@@ -18,6 +20,7 @@ export function ConfirmationModal({
|
||||
descriptionText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
buttonsIsDisabled = false,
|
||||
...props
|
||||
}: ConfirmationModalProps) {
|
||||
const handleCancelClick = () => {
|
||||
@@ -38,7 +41,11 @@ export function ConfirmationModal({
|
||||
<Button theme="outline" onClick={handleCancelClick}>
|
||||
{cancelButtonLabel}
|
||||
</Button>
|
||||
<Button theme="danger" onClick={onConfirm}>
|
||||
<Button
|
||||
theme="danger"
|
||||
disabled={buttonsIsDisabled}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmButtonLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import "./game-card.scss";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "../badge/badge";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useState, useMemo } from "react";
|
||||
import { useFormat, useRepacks } from "@renderer/hooks";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
@@ -45,6 +45,15 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const firstThreeRepackers = useMemo(
|
||||
() => uniqueRepackers.slice(0, 3),
|
||||
[uniqueRepackers]
|
||||
);
|
||||
const remainingCount = useMemo(
|
||||
() => uniqueRepackers.length - 3,
|
||||
[uniqueRepackers]
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
@@ -68,15 +77,24 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
|
||||
{uniqueRepackers.length > 0 ? (
|
||||
<ul className="game-card__download-options">
|
||||
{uniqueRepackers.map((repacker) => (
|
||||
{firstThreeRepackers.map((repacker) => (
|
||||
<li key={repacker}>
|
||||
<Badge>{repacker}</Badge>
|
||||
</li>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<li>
|
||||
<Badge>
|
||||
+{remainingCount}{" "}
|
||||
{t("game_card:available", { count: remainingCount })}
|
||||
</Badge>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="game-card__no-download-label">{t("no_downloads")}</p>
|
||||
)}
|
||||
|
||||
<div className="game-card__specifics">
|
||||
<div className="game-card__specifics-item">
|
||||
<DownloadIcon />
|
||||
@@ -84,11 +102,10 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="game-card__specifics-item">
|
||||
<PeopleIcon />
|
||||
<span>
|
||||
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
|
||||
{stats ? numberFormatter.format(stats.playerCount) : "…"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PeopleIcon } from "@primer/octicons-react";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
@@ -9,19 +9,13 @@ import { Avatar } from "../avatar/avatar";
|
||||
import { AuthPage } from "@shared";
|
||||
import "./sidebar-profile.scss";
|
||||
|
||||
const LONG_POLLING_INTERVAL = 120_000;
|
||||
|
||||
export function SidebarProfile() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation("sidebar");
|
||||
|
||||
const {
|
||||
userDetails,
|
||||
friendRequestCount,
|
||||
showFriendsModal,
|
||||
syncFriendRequests,
|
||||
} = useUserDetails();
|
||||
const { userDetails, friendRequestCount, showFriendsModal } =
|
||||
useUserDetails();
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
@@ -34,16 +28,6 @@ export function SidebarProfile() {
|
||||
navigate(`/profile/${userDetails.id}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const pollingInterval = setInterval(() => {
|
||||
syncFriendRequests();
|
||||
}, LONG_POLLING_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(pollingInterval);
|
||||
};
|
||||
}, [syncFriendRequests]);
|
||||
|
||||
const friendsButton = useMemo(() => {
|
||||
if (!userDetails) return null;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { darkenColor } from "@renderer/helpers";
|
||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||
import type { UserProfile, UserStats } from "@types";
|
||||
import type { Badge, UserProfile, UserStats } from "@types";
|
||||
import { average } from "color.js";
|
||||
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
@@ -16,6 +16,7 @@ export interface UserProfileContext {
|
||||
getUserProfile: () => Promise<void>;
|
||||
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
||||
backgroundImage: string;
|
||||
badges: Badge[];
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
||||
@@ -28,6 +29,7 @@ export const userProfileContext = createContext<UserProfileContext>({
|
||||
getUserProfile: async () => {},
|
||||
setSelectedBackgroundImage: () => {},
|
||||
backgroundImage: "",
|
||||
badges: [],
|
||||
});
|
||||
|
||||
const { Provider } = userProfileContext;
|
||||
@@ -47,6 +49,7 @@ export function UserProfileContextProvider({
|
||||
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
const [badges, setBadges] = useState<Badge[]>([]);
|
||||
const [heroBackground, setHeroBackground] = useState(
|
||||
DEFAULT_USER_PROFILE_BACKGROUND
|
||||
);
|
||||
@@ -101,12 +104,18 @@ export function UserProfileContextProvider({
|
||||
});
|
||||
}, [navigate, getUserStats, showErrorToast, userId, t]);
|
||||
|
||||
const getBadges = useCallback(async () => {
|
||||
const badges = await window.electron.getBadges();
|
||||
setBadges(badges);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setUserProfile(null);
|
||||
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
|
||||
|
||||
getUserProfile();
|
||||
}, [getUserProfile]);
|
||||
getBadges();
|
||||
}, [getUserProfile, getBadges]);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
@@ -118,6 +127,7 @@ export function UserProfileContextProvider({
|
||||
setSelectedBackgroundImage,
|
||||
backgroundImage: getBackgroundImageUrl(),
|
||||
userStats,
|
||||
badges,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
32
src/renderer/src/declaration.d.ts
vendored
32
src/renderer/src/declaration.d.ts
vendored
@@ -30,6 +30,8 @@ import type {
|
||||
GameRunning,
|
||||
TorBoxUser,
|
||||
Theme,
|
||||
Badge,
|
||||
Auth,
|
||||
} from "@types";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type disk from "diskusage";
|
||||
@@ -86,6 +88,11 @@ declare global {
|
||||
getDevelopers: () => Promise<string[]>;
|
||||
|
||||
/* Library */
|
||||
toggleAutomaticCloudSync: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
automaticCloudSync: boolean
|
||||
) => Promise<void>;
|
||||
addGameToLibrary: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -142,6 +149,8 @@ declare global {
|
||||
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
/* User preferences */
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||
updateUserPreferences: (
|
||||
preferences: Partial<UserPreferences>
|
||||
@@ -150,14 +159,21 @@ declare global {
|
||||
enabled: boolean;
|
||||
minimized: boolean;
|
||||
}) => Promise<void>;
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
onExtractionComplete: (
|
||||
cb: (shop: GameShop, objectId: string) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Download sources */
|
||||
putDownloadSource: (
|
||||
objectIds: string[]
|
||||
) => Promise<{ fingerprint: string }>;
|
||||
createDownloadSources: (urls: string[]) => Promise<void>;
|
||||
removeDownloadSource: (url: string, removeAll?: boolean) => Promise<void>;
|
||||
getDownloadSources: () => Promise<
|
||||
Pick<DownloadSource, "url" | "createdAt" | "updatedAt">[]
|
||||
>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<disk.DiskUsage>;
|
||||
@@ -217,6 +233,12 @@ declare global {
|
||||
) => Promise<Electron.OpenDialogReturnValue>;
|
||||
showItemInFolder: (path: string) => Promise<void>;
|
||||
getFeatures: () => Promise<string[]>;
|
||||
getBadges: () => Promise<Badge[]>;
|
||||
canInstallCommonRedist: () => Promise<boolean>;
|
||||
installCommonRedist: () => Promise<void>;
|
||||
onCommonRedistProgress: (
|
||||
cb: (value: { log: string; complete: boolean }) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
platform: NodeJS.Platform;
|
||||
|
||||
/* Auto update */
|
||||
@@ -227,6 +249,7 @@ declare global {
|
||||
restartAndInstallUpdate: () => Promise<void>;
|
||||
|
||||
/* Auth */
|
||||
getAuth: () => Promise<Auth | null>;
|
||||
signOut: () => Promise<void>;
|
||||
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||
getSessionHash: () => Promise<string | null>;
|
||||
@@ -271,7 +294,10 @@ declare global {
|
||||
path: string
|
||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||
syncFriendRequests: () => Promise<FriendRequestSync>;
|
||||
syncFriendRequests: () => Promise<void>;
|
||||
onSyncFriendRequests: (
|
||||
cb: (friendRequests: FriendRequestSync) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
updateFriendRequest: (
|
||||
userId: string,
|
||||
action: FriendRequestAction
|
||||
|
||||
@@ -21,9 +21,9 @@ export interface CatalogueCache {
|
||||
|
||||
export const db = new Dexie("Hydra");
|
||||
|
||||
db.version(8).stores({
|
||||
db.version(9).stores({
|
||||
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, objectIds, createdAt, updatedAt`,
|
||||
downloadSources: `++id, url, name, etag, objectIds, downloadCount, status, fingerprint, createdAt, updatedAt`,
|
||||
downloadSources: `++id, &url, name, etag, objectIds, downloadCount, status, fingerprint, createdAt, updatedAt`,
|
||||
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import { formatDate, getDateLocale } from "@shared";
|
||||
import { format, formatDistance, subMilliseconds } from "date-fns";
|
||||
import type { FormatDistanceOptions } from "date-fns";
|
||||
import {
|
||||
ptBR,
|
||||
enUS,
|
||||
es,
|
||||
fr,
|
||||
pl,
|
||||
hu,
|
||||
tr,
|
||||
ru,
|
||||
it,
|
||||
be,
|
||||
zhCN,
|
||||
da,
|
||||
} from "date-fns/locale";
|
||||
import { enUS } from "date-fns/locale";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function useDate() {
|
||||
@@ -21,22 +9,6 @@ export function useDate() {
|
||||
|
||||
const { language } = i18n;
|
||||
|
||||
const getDateLocale = () => {
|
||||
if (language.startsWith("pt")) return ptBR;
|
||||
if (language.startsWith("es")) return es;
|
||||
if (language.startsWith("fr")) return fr;
|
||||
if (language.startsWith("hu")) return hu;
|
||||
if (language.startsWith("pl")) return pl;
|
||||
if (language.startsWith("tr")) return tr;
|
||||
if (language.startsWith("ru")) return ru;
|
||||
if (language.startsWith("it")) return it;
|
||||
if (language.startsWith("be")) return be;
|
||||
if (language.startsWith("zh")) return zhCN;
|
||||
if (language.startsWith("da")) return da;
|
||||
|
||||
return enUS;
|
||||
};
|
||||
|
||||
return {
|
||||
formatDistance: (
|
||||
date: string | number | Date,
|
||||
@@ -46,7 +18,7 @@ export function useDate() {
|
||||
try {
|
||||
return formatDistance(date, baseDate, {
|
||||
...options,
|
||||
locale: getDateLocale(),
|
||||
locale: getDateLocale(language),
|
||||
});
|
||||
} catch (err) {
|
||||
return "";
|
||||
@@ -61,7 +33,7 @@ export function useDate() {
|
||||
try {
|
||||
return formatDistance(subMilliseconds(new Date(), millis), baseDate, {
|
||||
...options,
|
||||
locale: getDateLocale(),
|
||||
locale: getDateLocale(language),
|
||||
});
|
||||
} catch (err) {
|
||||
return "";
|
||||
@@ -69,18 +41,13 @@ export function useDate() {
|
||||
},
|
||||
|
||||
formatDateTime: (date: number | Date | string): string => {
|
||||
const locale = getDateLocale();
|
||||
const locale = getDateLocale(language);
|
||||
return format(
|
||||
date,
|
||||
locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy HH:mm"
|
||||
);
|
||||
},
|
||||
|
||||
formatDate: (date: number | Date | string): string => {
|
||||
if (isNaN(new Date(date).getDate())) return "N/A";
|
||||
|
||||
const locale = getDateLocale();
|
||||
return format(date, locale == enUS ? "MM/dd/yyyy" : "dd/MM/yyyy");
|
||||
},
|
||||
formatDate: (date: number | Date | string) => formatDate(date, language),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
setFriendRequests,
|
||||
setFriendsModalVisible,
|
||||
setFriendsModalHidden,
|
||||
setFriendRequestCount,
|
||||
} from "@renderer/features";
|
||||
import type {
|
||||
FriendRequestAction,
|
||||
@@ -15,7 +14,6 @@ import type {
|
||||
} from "@types";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
|
||||
export function useUserDetails() {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -89,24 +87,15 @@ export function useUserDetails() {
|
||||
]
|
||||
);
|
||||
|
||||
const syncFriendRequests = useCallback(async () => {
|
||||
return window.electron
|
||||
.syncFriendRequests()
|
||||
.then((sync) => {
|
||||
dispatch(setFriendRequestCount(sync.friendRequestCount));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [dispatch]);
|
||||
|
||||
const fetchFriendRequests = useCallback(async () => {
|
||||
return window.electron
|
||||
.getFriendRequests()
|
||||
.then((friendRequests) => {
|
||||
syncFriendRequests();
|
||||
window.electron.syncFriendRequests();
|
||||
dispatch(setFriendRequests(friendRequests));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [dispatch, syncFriendRequests]);
|
||||
}, [dispatch]);
|
||||
|
||||
const showFriendsModal = useCallback(
|
||||
(initialTab: UserFriendModalTab, userId: string) => {
|
||||
@@ -146,8 +135,8 @@ export function useUserDetails() {
|
||||
const unblockUser = (userId: string) => window.electron.unblockUser(userId);
|
||||
|
||||
const hasActiveSubscription = useMemo(() => {
|
||||
const expiresAt = userDetails?.subscription?.expiresAt;
|
||||
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
|
||||
const expiresAt = new Date(userDetails?.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
}, [userDetails]);
|
||||
|
||||
return {
|
||||
@@ -168,7 +157,6 @@ export function useUserDetails() {
|
||||
patchUser,
|
||||
sendFriendRequest,
|
||||
fetchFriendRequests,
|
||||
syncFriendRequests,
|
||||
updateFriendRequestState,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
|
||||
@@ -11,6 +11,7 @@ import "@fontsource/noto-sans/500.css";
|
||||
import "@fontsource/noto-sans/700.css";
|
||||
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import "react-tooltip/dist/react-tooltip.css";
|
||||
|
||||
import { App } from "./app";
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ $logo-max-width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: globals.$spacing-unit / 2;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
color: globals.$body-color;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@@ -18,14 +18,11 @@ export function Pagination({
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
// Number of visible pages
|
||||
const visiblePages = 3;
|
||||
|
||||
// Calculate the start and end of the visible range
|
||||
let startPage = Math.max(1, page - 1); // Shift range slightly back
|
||||
let startPage = Math.max(1, page - 1);
|
||||
let endPage = startPage + visiblePages - 1;
|
||||
|
||||
// Adjust the range if we're near the start or end
|
||||
if (endPage > totalPages) {
|
||||
endPage = totalPages;
|
||||
startPage = Math.max(1, endPage - visiblePages + 1);
|
||||
@@ -33,7 +30,6 @@ export function Pagination({
|
||||
|
||||
return (
|
||||
<div className="pagination">
|
||||
{/* Previous Button */}
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
@@ -45,7 +41,6 @@ export function Pagination({
|
||||
|
||||
{page > 2 && (
|
||||
<>
|
||||
{/* initial page */}
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(1)}
|
||||
@@ -55,14 +50,12 @@ export function Pagination({
|
||||
{1}
|
||||
</Button>
|
||||
|
||||
{/* ellipsis */}
|
||||
<div className="pagination__ellipsis">
|
||||
<span className="pagination__ellipsis-text">...</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Page Buttons */}
|
||||
{Array.from(
|
||||
{ length: endPage - startPage + 1 },
|
||||
(_, i) => startPage + i
|
||||
@@ -79,12 +72,10 @@ export function Pagination({
|
||||
|
||||
{page < totalPages - 1 && (
|
||||
<>
|
||||
{/* ellipsis */}
|
||||
<div className="pagination__ellipsis">
|
||||
<span className="pagination__ellipsis-text">...</span>
|
||||
</div>
|
||||
|
||||
{/* last page */}
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
@@ -96,7 +87,6 @@ export function Pagination({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Next Button */}
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
|
||||
import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||
|
||||
import "./download-group.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import {
|
||||
ColumnsIcon,
|
||||
DownloadIcon,
|
||||
FileDirectoryIcon,
|
||||
LinkIcon,
|
||||
PlayIcon,
|
||||
QuestionIcon,
|
||||
@@ -56,6 +57,8 @@ export function DownloadGroup({
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
const {
|
||||
lastPacket,
|
||||
progress,
|
||||
@@ -89,6 +92,14 @@ export function DownloadGroup({
|
||||
return map;
|
||||
}, [seedingStatus]);
|
||||
|
||||
const extractGameDownload = useCallback(
|
||||
async (shop: GameShop, objectId: string) => {
|
||||
await window.electron.extractGameDownload(shop, objectId);
|
||||
updateLibrary();
|
||||
},
|
||||
[updateLibrary]
|
||||
);
|
||||
|
||||
const getGameInfo = (game: LibraryGame) => {
|
||||
const download = game.download!;
|
||||
|
||||
@@ -96,6 +107,10 @@ export function DownloadGroup({
|
||||
const finalDownloadSize = getFinalDownloadSize(game);
|
||||
const seedingStatus = seedingMap.get(game.id);
|
||||
|
||||
if (download.extracting) {
|
||||
return <p>{t("extracting")}</p>;
|
||||
}
|
||||
|
||||
if (isGameDeleting(game.id)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
}
|
||||
@@ -197,6 +212,14 @@ export function DownloadGroup({
|
||||
},
|
||||
icon: <DownloadIcon />,
|
||||
},
|
||||
{
|
||||
label: t("extract"),
|
||||
disabled: game.download.extracting,
|
||||
icon: <FileDirectoryIcon />,
|
||||
onClick: () => {
|
||||
extractGameDownload(game.shop, game.objectId);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("stop_seeding"),
|
||||
disabled: deleting,
|
||||
|
||||
@@ -38,7 +38,13 @@ export default function Downloads() {
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.onSeedingStatus((value) => setSeedingStatus(value));
|
||||
}, []);
|
||||
|
||||
const unsubscribe = window.electron.onExtractionComplete(() => {
|
||||
updateLibrary();
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [updateLibrary]);
|
||||
|
||||
const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
|
||||
window.electron.openGameInstaller(shop, objectId).then((isBinaryInPath) => {
|
||||
@@ -67,7 +73,7 @@ export default function Downloads() {
|
||||
if (!next.download) return prev;
|
||||
|
||||
/* Is downloading */
|
||||
if (lastPacket?.gameId === next.id)
|
||||
if (lastPacket?.gameId === next.id || next.download.extracting)
|
||||
return { ...prev, downloading: [...prev.downloading, next] };
|
||||
|
||||
/* Is either queued or paused */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user