mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Merge branch 'main' into feat/unzip-behavior-option
This commit is contained in:
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -33,9 +33,9 @@ body:
|
||||
attributes:
|
||||
label: Additional information and data
|
||||
description: |
|
||||
Add screenshots and upload your logs file here.
|
||||
Logs location on Windows: "%appdata%/hydra"
|
||||
Logs location on Linux: "~/.config/hydra/"
|
||||
Add screenshots and upload your all logs file here.
|
||||
Logs location on Windows: "%appdata%/hydralauncher/logs"
|
||||
Logs location on Linux: "~/.config/hydralauncher/logs"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@@ -25,23 +25,13 @@ jobs:
|
||||
node-version: 20.18.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
|
||||
- name: Build Rust
|
||||
run: cargo build --release
|
||||
working-directory: ./rust_rpc
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
node-version: 20.18.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Validate current commit (last commit) with commitlint
|
||||
run: npx commitlint --last --verbose
|
||||
|
||||
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@@ -26,23 +26,13 @@ jobs:
|
||||
node-version: 20.18.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
|
||||
- name: Build Rust
|
||||
run: cargo build --release
|
||||
working-directory: ./rust_rpc
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
@@ -98,6 +88,18 @@ jobs:
|
||||
dist/*.blockmap
|
||||
dist/*.pacman
|
||||
|
||||
- name: Upload build
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
|
||||
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
|
||||
S3_BUILDS_BUCKET_NAME: ${{ secrets.S3_BUILDS_BUCKET_NAME }}
|
||||
BUILDS_URL: ${{ secrets.BUILDS_URL }}
|
||||
BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
run: node scripts/upload-build.cjs
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,5 +16,4 @@ hydra-python-rpc/
|
||||
|
||||
*storybook.log
|
||||
|
||||
|
||||
target/
|
||||
aria2/
|
||||
|
||||
0
binaries/7zz
Normal file → Executable file
0
binaries/7zz
Normal file → Executable file
0
binaries/7zzs
Normal file → Executable file
0
binaries/7zzs
Normal file → Executable file
Binary file not shown.
@@ -3,6 +3,7 @@ productName: Hydra
|
||||
directories:
|
||||
buildResources: build
|
||||
extraResources:
|
||||
- aria2
|
||||
- ludusavi
|
||||
- hydra-python-rpc
|
||||
- seeds
|
||||
@@ -22,7 +23,6 @@ win:
|
||||
extraResources:
|
||||
- from: binaries/7z.exe
|
||||
- from: binaries/7z.dll
|
||||
- from: rust_rpc/target/release/hydra-httpdl.exe
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
@@ -40,7 +40,6 @@ mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extraResources:
|
||||
- from: binaries/7zz
|
||||
- from: rust_rpc/target/release/hydra-httpdl
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
@@ -52,7 +51,6 @@ dmg:
|
||||
linux:
|
||||
extraResources:
|
||||
- from: binaries/7zzs
|
||||
- from: rust_rpc/target/release/hydra-httpdl
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.4.2",
|
||||
"version": "3.4.4",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -21,7 +21,7 @@
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "cargo build --manifest-path=rust_rpc/Cargo.toml && electron-vite dev",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps && node ./scripts/postinstall.cjs",
|
||||
"build:unpack": "npm run build && electron-builder --dir",
|
||||
@@ -48,7 +48,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
"color.js": "^1.2.0",
|
||||
"create-desktop-shortcuts": "^1.11.0",
|
||||
"create-desktop-shortcuts": "^1.11.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dexie": "^4.0.10",
|
||||
"diskusage": "^1.2.0",
|
||||
@@ -59,7 +59,6 @@
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kill-port": "^2.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
"piscina": "^4.7.0",
|
||||
|
||||
@@ -1,94 +1,48 @@
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
import aria2p
|
||||
|
||||
class HttpDownloader:
|
||||
def __init__(self, hydra_httpdl_bin: str):
|
||||
self.hydra_exe = hydra_httpdl_bin
|
||||
self.process = None
|
||||
self.last_status = None
|
||||
|
||||
def start_download(self, url: str, save_path: str, header: str = None, allow_multiple_connections: bool = False, connections_limit: int = 1):
|
||||
cmd = [self.hydra_exe]
|
||||
|
||||
cmd.append(url)
|
||||
|
||||
cmd.extend([
|
||||
"--chunk-size", "10",
|
||||
"--buffer-size", "16",
|
||||
"--force-download",
|
||||
"--log",
|
||||
"--silent"
|
||||
])
|
||||
|
||||
if header:
|
||||
cmd.extend(["--header", header])
|
||||
|
||||
if allow_multiple_connections:
|
||||
cmd.extend(["--connections", str(connections_limit)])
|
||||
else:
|
||||
cmd.extend(["--connections", "1"])
|
||||
|
||||
print(f"running hydra-httpdl: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=save_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True
|
||||
def __init__(self):
|
||||
self.download = None
|
||||
self.aria2 = aria2p.API(
|
||||
aria2p.Client(
|
||||
host="http://localhost",
|
||||
port=6800,
|
||||
secret=""
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"error running hydra-httpdl: {e}")
|
||||
return None
|
||||
)
|
||||
|
||||
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 get_download_status(self):
|
||||
|
||||
if not self.process:
|
||||
if self.download == None:
|
||||
return None
|
||||
|
||||
try:
|
||||
line = self.process.stdout.readline()
|
||||
if line:
|
||||
status = json.loads(line.strip())
|
||||
self.last_status = status
|
||||
elif self.last_status:
|
||||
status = self.last_status
|
||||
else:
|
||||
return None
|
||||
|
||||
response = {
|
||||
"status": "active",
|
||||
"progress": status["progress"],
|
||||
"downloadSpeed": status["speed_bps"],
|
||||
"numPeers": 0,
|
||||
"numSeeds": 0,
|
||||
"bytesDownloaded": status["downloaded_bytes"],
|
||||
"fileSize": status["total_bytes"],
|
||||
"folderName": status["filename"]
|
||||
}
|
||||
|
||||
if status["progress"] == 1:
|
||||
response["status"] = "complete"
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print(f"error getting download status: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def stop_download(self):
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process = None
|
||||
self.last_status = None
|
||||
|
||||
def pause_download(self):
|
||||
self.stop_download()
|
||||
|
||||
def cancel_download(self):
|
||||
self.stop_download()
|
||||
|
||||
download = self.aria2.get_download(self.download.gid)
|
||||
|
||||
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
|
||||
|
||||
@@ -13,7 +13,6 @@ http_port = sys.argv[2]
|
||||
rpc_password = sys.argv[3]
|
||||
start_download_payload = sys.argv[4]
|
||||
start_seeding_payload = sys.argv[5]
|
||||
hydra_httpdl_bin = sys.argv[6]
|
||||
|
||||
downloads = {}
|
||||
# This can be streamed down from Node
|
||||
@@ -33,10 +32,10 @@ if start_download_payload:
|
||||
except Exception as e:
|
||||
print("Error starting torrent download", e)
|
||||
else:
|
||||
http_downloader = HttpDownloader(hydra_httpdl_bin)
|
||||
http_downloader = HttpDownloader()
|
||||
downloads[initial_download['game_id']] = http_downloader
|
||||
try:
|
||||
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get('allow_multiple_connections', False), initial_download.get('connections_limit', 24))
|
||||
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get('out'))
|
||||
except Exception as e:
|
||||
print("Error starting http download", e)
|
||||
|
||||
@@ -148,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('allow_multiple_connections', False), data.get('connections_limit', 24))
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||
else:
|
||||
http_downloader = HttpDownloader(hydra_httpdl_bin)
|
||||
http_downloader = HttpDownloader()
|
||||
downloads[game_id] = http_downloader
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('allow_multiple_connections', False), data.get('connections_limit', 24))
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||
|
||||
downloading_game_id = game_id
|
||||
|
||||
@@ -183,4 +182,3 @@ def action():
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=int(http_port))
|
||||
|
||||
@@ -5,3 +5,4 @@ pywin32; sys_platform == 'win32'
|
||||
psutil
|
||||
Pillow
|
||||
flask
|
||||
aria2p
|
||||
|
||||
2040
rust_rpc/Cargo.lock
generated
2040
rust_rpc/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "hydra-httpdl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full", "macros", "rt-multi-thread"] }
|
||||
reqwest = { version = "0.12.5", features = ["stream"] }
|
||||
futures = "0.3"
|
||||
bytes = "1.4"
|
||||
indicatif = "0.17"
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
urlencoding = "2.1"
|
||||
serde_json = "1.0"
|
||||
bitvec = "1.0"
|
||||
sha2 = "0.10"
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
@@ -1,966 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use bitvec::prelude::*;
|
||||
use clap::Parser;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const DEFAULT_MAX_RETRIES: usize = 3;
|
||||
const RETRY_BACKOFF_MS: u64 = 500;
|
||||
const DEFAULT_OUTPUT_FILENAME: &str = "output.bin";
|
||||
const DEFAULT_CONNECTIONS: usize = 16;
|
||||
const DEFAULT_CHUNK_SIZE_MB: usize = 5;
|
||||
const DEFAULT_BUFFER_SIZE_MB: usize = 8;
|
||||
const DEFAULT_VERBOSE: bool = false;
|
||||
const DEFAULT_SILENT: bool = false;
|
||||
const DEFAULT_LOG: bool = false;
|
||||
const DEFAULT_FORCE_NEW: bool = false;
|
||||
const DEFAULT_RESUME_ONLY: bool = false;
|
||||
const DEFAULT_FORCE_DOWNLOAD: bool = false;
|
||||
const HEADER_SIZE: usize = 4096;
|
||||
const MAGIC_NUMBER: &[u8; 5] = b"HYDRA";
|
||||
const FORMAT_VERSION: u8 = 1;
|
||||
const FINALIZE_BUFFER_SIZE: usize = 1024 * 1024;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "hydra-httpdl")]
|
||||
#[command(author = "los-broxas")]
|
||||
#[command(version = "0.2.0")]
|
||||
#[command(about = "high speed and low resource usage http downloader with resume capability", long_about = None)]
|
||||
struct CliArgs {
|
||||
/// file url to download
|
||||
#[arg(required = true)]
|
||||
url: String,
|
||||
|
||||
/// output file path (or directory to save with original filename)
|
||||
#[arg(default_value = DEFAULT_OUTPUT_FILENAME)]
|
||||
output: String,
|
||||
|
||||
/// number of concurrent connections for parallel download
|
||||
#[arg(short = 'c', long, default_value_t = DEFAULT_CONNECTIONS)]
|
||||
connections: usize,
|
||||
|
||||
/// chunk size in MB for each connection
|
||||
#[arg(short = 'k', long, default_value_t = DEFAULT_CHUNK_SIZE_MB)]
|
||||
chunk_size: usize,
|
||||
|
||||
/// buffer size in MB for file writing
|
||||
#[arg(short, long, default_value_t = DEFAULT_BUFFER_SIZE_MB)]
|
||||
buffer_size: usize,
|
||||
|
||||
/// show detailed progress information
|
||||
#[arg(short = 'v', long, default_value_t = DEFAULT_VERBOSE)]
|
||||
verbose: bool,
|
||||
|
||||
/// suppress progress bar
|
||||
#[arg(short = 's', long, default_value_t = DEFAULT_SILENT)]
|
||||
silent: bool,
|
||||
|
||||
/// log download statistics in JSON format every second
|
||||
#[arg(short = 'l', long, default_value_t = DEFAULT_LOG)]
|
||||
log: bool,
|
||||
|
||||
/// force new download, ignore existing partial files
|
||||
#[arg(short = 'f', long, default_value_t = DEFAULT_FORCE_NEW)]
|
||||
force_new: bool,
|
||||
|
||||
/// only resume existing download, exit if no partial file exists
|
||||
#[arg(short = 'r', long, default_value_t = DEFAULT_RESUME_ONLY)]
|
||||
resume_only: bool,
|
||||
|
||||
/// force download, ignore some verification checks
|
||||
#[arg(short = 'F', long, default_value_t = DEFAULT_FORCE_DOWNLOAD)]
|
||||
force_download: bool,
|
||||
|
||||
/// HTTP headers to send with request (format: "Key: Value")
|
||||
#[arg(short = 'H', long)]
|
||||
header: Vec<String>,
|
||||
}
|
||||
|
||||
struct DownloadConfig {
|
||||
url: String,
|
||||
output_path: String,
|
||||
num_connections: usize,
|
||||
chunk_size: usize,
|
||||
buffer_size: usize,
|
||||
verbose: bool,
|
||||
silent: bool,
|
||||
log: bool,
|
||||
force_new: bool,
|
||||
resume_only: bool,
|
||||
headers: Vec<String>,
|
||||
force_download: bool,
|
||||
}
|
||||
|
||||
impl DownloadConfig {
|
||||
fn should_log(&self) -> bool {
|
||||
self.verbose && !self.silent
|
||||
}
|
||||
|
||||
fn should_log_stats(&self) -> bool {
|
||||
self.log
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadStats {
|
||||
progress_percent: f64,
|
||||
bytes_downloaded: u64,
|
||||
total_size: u64,
|
||||
speed_bytes_per_sec: f64,
|
||||
eta_seconds: u64,
|
||||
elapsed_seconds: u64,
|
||||
}
|
||||
|
||||
struct HydraHeader {
|
||||
magic: [u8; 5], // "HYDRA" identifier
|
||||
version: u8, // header version
|
||||
file_size: u64, // file size
|
||||
etag: [u8; 32], // etag hash
|
||||
url_hash: [u8; 32], // url hash
|
||||
chunk_size: u32, // chunk size
|
||||
chunk_count: u32, // chunk count
|
||||
chunks_bitmap: BitVec<u8>, // chunks bitmap
|
||||
}
|
||||
|
||||
impl HydraHeader {
|
||||
fn new(file_size: u64, etag: &str, url: &str, chunk_size: u32) -> Self {
|
||||
let chunk_count = ((file_size as f64) / (chunk_size as f64)).ceil() as u32;
|
||||
let chunks_bitmap = bitvec![u8, Lsb0; 0; chunk_count as usize];
|
||||
|
||||
let mut etag_hash = [0u8; 32];
|
||||
let etag_digest = Sha256::digest(etag.as_bytes());
|
||||
etag_hash.copy_from_slice(&etag_digest[..]);
|
||||
|
||||
let mut url_hash = [0u8; 32];
|
||||
let url_digest = Sha256::digest(url.as_bytes());
|
||||
url_hash.copy_from_slice(&url_digest[..]);
|
||||
|
||||
Self {
|
||||
magic: *MAGIC_NUMBER,
|
||||
version: FORMAT_VERSION,
|
||||
file_size,
|
||||
etag: etag_hash,
|
||||
url_hash,
|
||||
chunk_size,
|
||||
chunk_count,
|
||||
chunks_bitmap,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_file<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_all(&self.magic)?;
|
||||
writer.write_all(&[self.version])?;
|
||||
writer.write_all(&self.file_size.to_le_bytes())?;
|
||||
writer.write_all(&self.etag)?;
|
||||
writer.write_all(&self.url_hash)?;
|
||||
writer.write_all(&self.chunk_size.to_le_bytes())?;
|
||||
writer.write_all(&self.chunk_count.to_le_bytes())?;
|
||||
|
||||
let bitmap_bytes = self.chunks_bitmap.as_raw_slice();
|
||||
writer.write_all(bitmap_bytes)?;
|
||||
|
||||
let header_size = 5 + 1 + 8 + 32 + 32 + 4 + 4 + bitmap_bytes.len();
|
||||
let padding_size = HEADER_SIZE - header_size;
|
||||
let padding = vec![0u8; padding_size];
|
||||
writer.write_all(&padding)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_file<R: Read + Seek>(reader: &mut R) -> Result<Self> {
|
||||
let mut magic = [0u8; 5];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
if magic != *MAGIC_NUMBER {
|
||||
anyhow::bail!("Not a valid Hydra download file");
|
||||
}
|
||||
|
||||
let mut version = [0u8; 1];
|
||||
reader.read_exact(&mut version)?;
|
||||
|
||||
if version[0] != FORMAT_VERSION {
|
||||
anyhow::bail!("Incompatible format version");
|
||||
}
|
||||
|
||||
let mut file_size_bytes = [0u8; 8];
|
||||
reader.read_exact(&mut file_size_bytes)?;
|
||||
let file_size = u64::from_le_bytes(file_size_bytes);
|
||||
|
||||
let mut etag = [0u8; 32];
|
||||
reader.read_exact(&mut etag)?;
|
||||
|
||||
let mut url_hash = [0u8; 32];
|
||||
reader.read_exact(&mut url_hash)?;
|
||||
|
||||
let mut chunk_size_bytes = [0u8; 4];
|
||||
reader.read_exact(&mut chunk_size_bytes)?;
|
||||
let chunk_size = u32::from_le_bytes(chunk_size_bytes);
|
||||
|
||||
let mut chunk_count_bytes = [0u8; 4];
|
||||
reader.read_exact(&mut chunk_count_bytes)?;
|
||||
let chunk_count = u32::from_le_bytes(chunk_count_bytes);
|
||||
|
||||
let bitmap_bytes_len = (chunk_count as usize + 7) / 8;
|
||||
let mut bitmap_bytes = vec![0u8; bitmap_bytes_len];
|
||||
reader.read_exact(&mut bitmap_bytes)?;
|
||||
|
||||
let chunks_bitmap = BitVec::<u8, Lsb0>::from_vec(bitmap_bytes);
|
||||
|
||||
reader.seek(SeekFrom::Start(HEADER_SIZE as u64))?;
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version: version[0],
|
||||
file_size,
|
||||
etag,
|
||||
url_hash,
|
||||
chunk_size,
|
||||
chunk_count,
|
||||
chunks_bitmap,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_chunk_complete(&mut self, chunk_index: usize) -> Result<()> {
|
||||
if chunk_index >= self.chunk_count as usize {
|
||||
anyhow::bail!("Chunk index out of bounds");
|
||||
}
|
||||
|
||||
self.chunks_bitmap.set(chunk_index, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_chunk_complete(&self, chunk_index: usize) -> bool {
|
||||
if chunk_index >= self.chunk_count as usize {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.chunks_bitmap[chunk_index]
|
||||
}
|
||||
|
||||
fn get_incomplete_chunks(&self) -> Vec<(u64, u64)> {
|
||||
let incomplete_count = self.chunk_count as usize - self.chunks_bitmap.count_ones();
|
||||
let mut chunks = Vec::with_capacity(incomplete_count);
|
||||
let chunk_size = self.chunk_size as u64;
|
||||
|
||||
for i in 0..self.chunk_count as usize {
|
||||
if !self.is_chunk_complete(i) {
|
||||
let start = i as u64 * chunk_size;
|
||||
let end = std::cmp::min((i as u64 + 1) * chunk_size - 1, self.file_size - 1);
|
||||
chunks.push((start, end));
|
||||
}
|
||||
}
|
||||
|
||||
chunks
|
||||
}
|
||||
|
||||
fn is_download_complete(&self) -> bool {
|
||||
self.chunks_bitmap.count_ones() == self.chunk_count as usize
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressTracker {
|
||||
bar: Option<ProgressBar>,
|
||||
}
|
||||
|
||||
impl ProgressTracker {
|
||||
fn new(file_size: u64, silent: bool, enable_stats: bool) -> Result<Self> {
|
||||
let bar = if !silent || enable_stats {
|
||||
let pb = ProgressBar::new(file_size);
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("[{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")?
|
||||
);
|
||||
if silent {
|
||||
pb.set_draw_target(indicatif::ProgressDrawTarget::hidden());
|
||||
}
|
||||
Some(pb)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self { bar })
|
||||
}
|
||||
|
||||
fn increment(&self, amount: u64) {
|
||||
if let Some(pb) = &self.bar {
|
||||
pb.inc(amount);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self) {
|
||||
if let Some(pb) = &self.bar {
|
||||
pb.finish_with_message("Download complete");
|
||||
}
|
||||
}
|
||||
|
||||
fn get_stats(&self) -> Option<DownloadStats> {
|
||||
if let Some(pb) = &self.bar {
|
||||
let position = pb.position();
|
||||
let total = pb.length().unwrap_or(1);
|
||||
|
||||
Some(DownloadStats {
|
||||
progress_percent: position as f64 / total as f64,
|
||||
bytes_downloaded: position,
|
||||
total_size: total,
|
||||
speed_bytes_per_sec: pb.per_sec(),
|
||||
eta_seconds: pb.eta().as_secs(),
|
||||
elapsed_seconds: pb.elapsed().as_secs(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Downloader {
|
||||
client: Client,
|
||||
config: DownloadConfig,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
async fn download(&self) -> Result<()> {
|
||||
let (file_size, filename, etag) = self.get_file_info().await?;
|
||||
let output_path = self.determine_output_path(filename);
|
||||
|
||||
if self.config.should_log() {
|
||||
println!("Detected filename: {}", output_path);
|
||||
}
|
||||
|
||||
let resume_manager = ResumeManager::try_from_file(
|
||||
&output_path,
|
||||
file_size,
|
||||
&etag,
|
||||
&self.config.url,
|
||||
self.config.chunk_size as u32,
|
||||
self.config.force_new,
|
||||
self.config.resume_only,
|
||||
)?;
|
||||
|
||||
let file = self.prepare_output_file(&output_path, file_size)?;
|
||||
let progress = ProgressTracker::new(file_size, self.config.silent, self.config.log)?;
|
||||
|
||||
let chunks = if resume_manager.is_download_complete() {
|
||||
if self.config.should_log() {
|
||||
println!("File is already fully downloaded, finalizing...");
|
||||
}
|
||||
resume_manager.finalize_download()?;
|
||||
return Ok(());
|
||||
} else {
|
||||
let completed_chunks = resume_manager.header.chunks_bitmap.count_ones() as u32;
|
||||
let total_chunks = resume_manager.header.chunk_count;
|
||||
|
||||
if completed_chunks > 0 {
|
||||
if self.config.should_log() {
|
||||
let percent_done = (completed_chunks as f64 / total_chunks as f64) * 100.0;
|
||||
println!("Resuming download: {:.1}% already downloaded", percent_done);
|
||||
}
|
||||
|
||||
if let Some(pb) = &progress.bar {
|
||||
let downloaded = file_size * completed_chunks as u64 / total_chunks as u64;
|
||||
pb.inc(downloaded);
|
||||
}
|
||||
}
|
||||
|
||||
resume_manager.get_incomplete_chunks()
|
||||
};
|
||||
|
||||
if self.config.should_log() {
|
||||
println!(
|
||||
"Downloading {} chunks of total {}",
|
||||
chunks.len(),
|
||||
resume_manager.header.chunk_count
|
||||
);
|
||||
}
|
||||
|
||||
self.process_chunks_with_resume(
|
||||
chunks,
|
||||
file,
|
||||
file_size,
|
||||
progress,
|
||||
output_path.clone(),
|
||||
resume_manager,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn determine_output_path(&self, filename: Option<String>) -> String {
|
||||
if Path::new(&self.config.output_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
== DEFAULT_OUTPUT_FILENAME
|
||||
&& filename.is_some()
|
||||
{
|
||||
filename.unwrap()
|
||||
} else {
|
||||
self.config.output_path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_output_file(&self, path: &str, size: u64) -> Result<Arc<Mutex<BufWriter<File>>>> {
|
||||
let file = if Path::new(path).exists() {
|
||||
OpenOptions::new().read(true).write(true).open(path)?
|
||||
} else {
|
||||
let file = File::create(path)?;
|
||||
file.set_len(HEADER_SIZE as u64 + size)?;
|
||||
file
|
||||
};
|
||||
|
||||
Ok(Arc::new(Mutex::new(BufWriter::with_capacity(
|
||||
self.config.buffer_size,
|
||||
file,
|
||||
))))
|
||||
}
|
||||
|
||||
async fn process_chunks_with_resume(
|
||||
&self,
|
||||
chunks: Vec<(u64, u64)>,
|
||||
file: Arc<Mutex<BufWriter<File>>>,
|
||||
_file_size: u64,
|
||||
progress: ProgressTracker,
|
||||
real_filename: String,
|
||||
resume_manager: ResumeManager,
|
||||
) -> Result<()> {
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
let log_progress = if self.config.should_log_stats() {
|
||||
let progress_clone = progress.bar.clone();
|
||||
let filename = real_filename.clone();
|
||||
|
||||
let (log_cancel_tx, mut log_cancel_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let log_task = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
|
||||
let tracker = ProgressTracker {
|
||||
bar: progress_clone,
|
||||
};
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
if let Some(stats) = tracker.get_stats() {
|
||||
let json_output = json!({
|
||||
"progress": stats.progress_percent,
|
||||
"speed_bps": stats.speed_bytes_per_sec,
|
||||
"downloaded_bytes": stats.bytes_downloaded,
|
||||
"total_bytes": stats.total_size,
|
||||
"eta_seconds": stats.eta_seconds,
|
||||
"elapsed_seconds": stats.elapsed_seconds,
|
||||
"filename": filename
|
||||
});
|
||||
println!("{}", json_output);
|
||||
}
|
||||
}
|
||||
_ = &mut log_cancel_rx => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Some((log_task, log_cancel_tx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let resume_manager = Arc::new(Mutex::new(resume_manager));
|
||||
|
||||
for (start, end) in chunks {
|
||||
let client = self.client.clone();
|
||||
let url = self.config.url.clone();
|
||||
let file_clone = Arc::clone(&file);
|
||||
let pb_clone = progress.bar.clone();
|
||||
let manager_clone = Arc::clone(&resume_manager);
|
||||
let headers = self.config.headers.clone();
|
||||
let force_download = self.config.force_download;
|
||||
let should_log = self.config.should_log();
|
||||
|
||||
let chunk_size = self.config.chunk_size as u64;
|
||||
let chunk_index = (start / chunk_size) as usize;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let result = Self::download_chunk_with_retry(
|
||||
client,
|
||||
url,
|
||||
start,
|
||||
end,
|
||||
file_clone,
|
||||
pb_clone,
|
||||
DEFAULT_MAX_RETRIES,
|
||||
&headers,
|
||||
force_download,
|
||||
should_log,
|
||||
)
|
||||
.await;
|
||||
|
||||
if result.is_ok() {
|
||||
let mut manager = manager_clone.lock().await;
|
||||
manager.set_chunk_complete(chunk_index)?;
|
||||
}
|
||||
|
||||
result
|
||||
}));
|
||||
|
||||
if tasks.len() >= self.config.num_connections {
|
||||
if let Some(result) = tasks.next().await {
|
||||
result??;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(result) = tasks.next().await {
|
||||
result??;
|
||||
}
|
||||
|
||||
{
|
||||
let mut writer = file.lock().await;
|
||||
writer.flush()?;
|
||||
}
|
||||
|
||||
progress.finish();
|
||||
|
||||
if let Some((log_handle, log_cancel_tx)) = log_progress {
|
||||
if self.config.should_log_stats() {
|
||||
let json_output = json!({
|
||||
"progress": 1.0,
|
||||
"speed_bps": 0.0,
|
||||
"downloaded_bytes": _file_size,
|
||||
"total_bytes": _file_size,
|
||||
"eta_seconds": 0,
|
||||
"elapsed_seconds": if let Some(pb) = &progress.bar { pb.elapsed().as_secs() } else { 0 },
|
||||
"filename": real_filename
|
||||
});
|
||||
println!("{}", json_output);
|
||||
}
|
||||
|
||||
let _ = log_cancel_tx.send(());
|
||||
let _ = log_handle.await;
|
||||
}
|
||||
|
||||
let manager = resume_manager.lock().await;
|
||||
if manager.is_download_complete() {
|
||||
if self.config.should_log() {
|
||||
println!("Download complete, finalizing file...");
|
||||
}
|
||||
manager.finalize_download()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_chunk_with_retry(
|
||||
client: Client,
|
||||
url: String,
|
||||
start: u64,
|
||||
end: u64,
|
||||
file: Arc<Mutex<BufWriter<File>>>,
|
||||
progress_bar: Option<ProgressBar>,
|
||||
max_retries: usize,
|
||||
headers: &[String],
|
||||
force_download: bool,
|
||||
should_log: bool,
|
||||
) -> Result<()> {
|
||||
let mut retries = 0;
|
||||
loop {
|
||||
match Self::download_chunk(
|
||||
client.clone(),
|
||||
url.clone(),
|
||||
start,
|
||||
end,
|
||||
file.clone(),
|
||||
progress_bar.clone(),
|
||||
headers,
|
||||
force_download,
|
||||
should_log,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
retries += 1;
|
||||
if retries >= max_retries {
|
||||
return Err(e);
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(
|
||||
RETRY_BACKOFF_MS * (2_u64.pow(retries as u32 - 1)),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_chunk(
|
||||
client: Client,
|
||||
url: String,
|
||||
start: u64,
|
||||
end: u64,
|
||||
file: Arc<Mutex<BufWriter<File>>>,
|
||||
progress_bar: Option<ProgressBar>,
|
||||
headers: &[String],
|
||||
force_download: bool,
|
||||
should_log: bool,
|
||||
) -> Result<()> {
|
||||
let mut req = client
|
||||
.get(&url)
|
||||
.header("Range", format!("bytes={}-{}", start, end));
|
||||
|
||||
for header in headers {
|
||||
if let Some(idx) = header.find(':') {
|
||||
let (name, value) = header.split_at(idx);
|
||||
let value = value[1..].trim();
|
||||
req = req.header(name.trim(), value);
|
||||
}
|
||||
}
|
||||
|
||||
let resp = req.send().await?;
|
||||
|
||||
if resp.status() != StatusCode::PARTIAL_CONTENT && resp.status() != StatusCode::OK {
|
||||
if !force_download {
|
||||
anyhow::bail!("Server does not support Range requests");
|
||||
} else if should_log {
|
||||
println!("Server does not support Range requests, ignoring...");
|
||||
}
|
||||
}
|
||||
|
||||
let mut stream = resp.bytes_stream();
|
||||
let mut position = start;
|
||||
let mut total_bytes = 0;
|
||||
let expected_bytes = end - start + 1;
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result?;
|
||||
let chunk_size = chunk.len() as u64;
|
||||
|
||||
total_bytes += chunk_size;
|
||||
if total_bytes > expected_bytes {
|
||||
let remaining = expected_bytes - (total_bytes - chunk_size);
|
||||
let mut writer = file.lock().await;
|
||||
writer.seek(SeekFrom::Start(HEADER_SIZE as u64 + position))?;
|
||||
writer.write_all(&chunk[..remaining as usize])?;
|
||||
|
||||
let tracker = ProgressTracker {
|
||||
bar: progress_bar.clone(),
|
||||
};
|
||||
tracker.increment(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
let mut writer = file.lock().await;
|
||||
writer.seek(SeekFrom::Start(HEADER_SIZE as u64 + position))?;
|
||||
writer.write_all(&chunk)?;
|
||||
drop(writer);
|
||||
|
||||
position += chunk_size;
|
||||
let tracker = ProgressTracker {
|
||||
bar: progress_bar.clone(),
|
||||
};
|
||||
tracker.increment(chunk_size);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_file_info(&self) -> Result<(u64, Option<String>, String)> {
|
||||
let mut req = self.client.head(&self.config.url);
|
||||
|
||||
for header in &self.config.headers {
|
||||
if let Some(idx) = header.find(':') {
|
||||
let (name, value) = header.split_at(idx);
|
||||
let value = value[1..].trim();
|
||||
req = req.header(name.trim(), value);
|
||||
}
|
||||
}
|
||||
|
||||
let resp = req.send().await?;
|
||||
|
||||
let accepts_ranges = resp
|
||||
.headers()
|
||||
.get("accept-ranges")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.contains("bytes"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !accepts_ranges {
|
||||
let range_check = self
|
||||
.client
|
||||
.get(&self.config.url)
|
||||
.header("Range", "bytes=0-0")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if range_check.status() != StatusCode::PARTIAL_CONTENT {
|
||||
if !self.config.force_download {
|
||||
anyhow::bail!(
|
||||
"Server does not support Range requests, cannot continue with parallel download"
|
||||
);
|
||||
} else if self.config.should_log() {
|
||||
println!("Server does not support Range requests, ignoring...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file_size = if let Some(content_length) = resp.headers().get("content-length") {
|
||||
content_length.to_str()?.parse()?
|
||||
} else {
|
||||
anyhow::bail!("Could not determine file size")
|
||||
};
|
||||
|
||||
let etag = if let Some(etag_header) = resp.headers().get("etag") {
|
||||
etag_header.to_str()?.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"no-etag-{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
)
|
||||
};
|
||||
|
||||
let filename = self.extract_filename_from_response(&resp);
|
||||
|
||||
Ok((file_size, filename, etag))
|
||||
}
|
||||
|
||||
fn extract_filename_from_response(&self, resp: &reqwest::Response) -> Option<String> {
|
||||
if let Some(disposition) = resp.headers().get("content-disposition") {
|
||||
if let Ok(disposition_str) = disposition.to_str() {
|
||||
if let Some(filename) = Self::parse_content_disposition(disposition_str) {
|
||||
return Some(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::extract_filename_from_url(&self.config.url)
|
||||
}
|
||||
|
||||
fn parse_content_disposition(disposition: &str) -> Option<String> {
|
||||
if let Some(idx) = disposition.find("filename=") {
|
||||
let start = idx + 9;
|
||||
let mut end = disposition.len();
|
||||
|
||||
if disposition.as_bytes().get(start) == Some(&b'"') {
|
||||
let quoted_name = &disposition[start + 1..];
|
||||
if let Some(quote_end) = quoted_name.find('"') {
|
||||
return Some(quoted_name[..quote_end].to_string());
|
||||
}
|
||||
} else {
|
||||
if let Some(semicolon) = disposition[start..].find(';') {
|
||||
end = start + semicolon;
|
||||
}
|
||||
return Some(disposition[start..end].to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_filename_from_url(url: &str) -> Option<String> {
|
||||
if let Ok(parsed_url) = Url::parse(url) {
|
||||
let path = parsed_url.path();
|
||||
if let Some(path_filename) = Path::new(path).file_name() {
|
||||
if let Some(filename_str) = path_filename.to_str() {
|
||||
if !filename_str.is_empty() {
|
||||
if let Ok(decoded) = urlencoding::decode(filename_str) {
|
||||
return Some(decoded.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct ResumeManager {
|
||||
header: HydraHeader,
|
||||
file_path: String,
|
||||
}
|
||||
|
||||
impl ResumeManager {
|
||||
fn try_from_file(
|
||||
path: &str,
|
||||
file_size: u64,
|
||||
etag: &str,
|
||||
url: &str,
|
||||
chunk_size: u32,
|
||||
force_new: bool,
|
||||
resume_only: bool,
|
||||
) -> Result<Self> {
|
||||
if force_new {
|
||||
if Path::new(path).exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
|
||||
return Self::create_new_file(path, file_size, etag, url, chunk_size);
|
||||
}
|
||||
|
||||
if let Ok(file) = File::open(path) {
|
||||
let mut reader = BufReader::new(file);
|
||||
match HydraHeader::read_from_file(&mut reader) {
|
||||
Ok(header) => {
|
||||
let current_url_hash = Sha256::digest(url.as_bytes());
|
||||
|
||||
let url_matches = header.url_hash == current_url_hash.as_slice();
|
||||
let size_matches = header.file_size == file_size;
|
||||
|
||||
if url_matches && size_matches {
|
||||
return Ok(Self {
|
||||
header,
|
||||
file_path: path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if resume_only {
|
||||
anyhow::bail!(
|
||||
"Existing file is not compatible and resume_only option is active"
|
||||
);
|
||||
}
|
||||
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
Err(e) => {
|
||||
if resume_only {
|
||||
return Err(anyhow::anyhow!("Could not read file to resume: {}", e));
|
||||
}
|
||||
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
} else if resume_only {
|
||||
anyhow::bail!("File not found and resume_only option is active");
|
||||
}
|
||||
|
||||
Self::create_new_file(path, file_size, etag, url, chunk_size)
|
||||
}
|
||||
|
||||
fn create_new_file(
|
||||
path: &str,
|
||||
file_size: u64,
|
||||
etag: &str,
|
||||
url: &str,
|
||||
chunk_size: u32,
|
||||
) -> Result<Self> {
|
||||
let header = HydraHeader::new(file_size, etag, url, chunk_size);
|
||||
let file = File::create(path)?;
|
||||
file.set_len(HEADER_SIZE as u64 + file_size)?;
|
||||
|
||||
let mut writer = BufWriter::new(file);
|
||||
header.write_to_file(&mut writer)?;
|
||||
writer.flush()?;
|
||||
|
||||
Ok(Self {
|
||||
header,
|
||||
file_path: path.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_incomplete_chunks(&self) -> Vec<(u64, u64)> {
|
||||
self.header.get_incomplete_chunks()
|
||||
}
|
||||
|
||||
fn set_chunk_complete(&mut self, chunk_index: usize) -> Result<()> {
|
||||
self.header.set_chunk_complete(chunk_index)?;
|
||||
|
||||
let file = OpenOptions::new().write(true).open(&self.file_path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
let bitmap_offset = 5 + 1 + 8 + 32 + 32 + 4 + 4;
|
||||
writer.seek(SeekFrom::Start(bitmap_offset as u64))?;
|
||||
|
||||
let bitmap_bytes = self.header.chunks_bitmap.as_raw_slice();
|
||||
writer.write_all(bitmap_bytes)?;
|
||||
writer.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_download_complete(&self) -> bool {
|
||||
self.header.is_download_complete()
|
||||
}
|
||||
|
||||
fn finalize_download(&self) -> Result<()> {
|
||||
if !self.is_download_complete() {
|
||||
anyhow::bail!("Download is not complete");
|
||||
}
|
||||
|
||||
let temp_path = format!("{}.tmp", self.file_path);
|
||||
let source = File::open(&self.file_path)?;
|
||||
let dest = File::create(&temp_path)?;
|
||||
|
||||
let mut reader = BufReader::with_capacity(FINALIZE_BUFFER_SIZE, source);
|
||||
let mut writer = BufWriter::with_capacity(FINALIZE_BUFFER_SIZE, dest);
|
||||
|
||||
reader.seek(SeekFrom::Start(HEADER_SIZE as u64))?;
|
||||
|
||||
std::io::copy(&mut reader, &mut writer)?;
|
||||
writer.flush()?;
|
||||
drop(writer);
|
||||
|
||||
match std::fs::rename(&temp_path, &self.file_path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => {
|
||||
let _ = std::fs::remove_file(&self.file_path);
|
||||
std::fs::rename(&temp_path, &self.file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = CliArgs::parse();
|
||||
|
||||
let config = DownloadConfig {
|
||||
url: args.url.clone(),
|
||||
output_path: args.output,
|
||||
num_connections: args.connections,
|
||||
chunk_size: args.chunk_size * 1024 * 1024,
|
||||
buffer_size: args.buffer_size * 1024 * 1024,
|
||||
verbose: args.verbose,
|
||||
silent: args.silent,
|
||||
log: args.log,
|
||||
force_new: args.force_new,
|
||||
resume_only: args.resume_only,
|
||||
headers: args.header,
|
||||
force_download: args.force_download,
|
||||
};
|
||||
|
||||
if config.force_new && config.resume_only {
|
||||
eprintln!("Error: --force-new and --resume-only options cannot be used together");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let downloader = Downloader {
|
||||
client: Client::new(),
|
||||
config,
|
||||
};
|
||||
|
||||
if downloader.config.should_log() {
|
||||
println!(
|
||||
"Starting download with {} connections, chunk size: {}MB, buffer: {}MB",
|
||||
downloader.config.num_connections, args.chunk_size, args.buffer_size
|
||||
);
|
||||
println!("URL: {}", args.url);
|
||||
|
||||
if downloader.config.force_new {
|
||||
println!("Forcing new download, ignoring existing files");
|
||||
} else if downloader.config.resume_only {
|
||||
println!("Only resuming existing download");
|
||||
} else {
|
||||
println!("Resuming download if possible");
|
||||
}
|
||||
}
|
||||
|
||||
downloader.download().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
const { default: axios } = require("axios");
|
||||
const tar = require("tar");
|
||||
const util = require("node:util");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const exec = util.promisify(require("node:child_process").exec);
|
||||
|
||||
const ludusaviVersion = "0.29.0";
|
||||
|
||||
const fileName = {
|
||||
win32: "ludusavi-v0.25.0-win64.zip",
|
||||
linux: "ludusavi-v0.25.0-linux.zip",
|
||||
darwin: "ludusavi-v0.25.0-mac.zip",
|
||||
win32: `ludusavi-v${ludusaviVersion}-win64.zip`,
|
||||
linux: `ludusavi-v${ludusaviVersion}-linux.tar.gz`,
|
||||
darwin: `ludusavi-v${ludusaviVersion}-mac.tar.gz`,
|
||||
};
|
||||
|
||||
const downloadLudusavi = async () => {
|
||||
@@ -18,7 +22,7 @@ const downloadLudusavi = async () => {
|
||||
}
|
||||
|
||||
const file = fileName[process.platform];
|
||||
const downloadUrl = `https://github.com/mtkennerly/ludusavi/releases/download/v0.25.0/${file}`;
|
||||
const downloadUrl = `https://github.com/mtkennerly/ludusavi/releases/download/v${ludusaviVersion}/${file}`;
|
||||
|
||||
console.log(`Downloading ${file}...`);
|
||||
|
||||
@@ -30,10 +34,18 @@ const downloadLudusavi = async () => {
|
||||
console.log(`Downloaded ${file}, extracting...`);
|
||||
|
||||
const pwd = process.cwd();
|
||||
|
||||
const targetPath = path.join(pwd, "ludusavi");
|
||||
|
||||
await exec(`npx extract-zip ${file} ${targetPath}`);
|
||||
await fs.promises.mkdir(targetPath, { recursive: true });
|
||||
|
||||
if (process.platform === "win32") {
|
||||
await exec(`npx extract-zip ${file} ${targetPath}`);
|
||||
} else {
|
||||
await tar.x({
|
||||
file: file,
|
||||
cwd: targetPath,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
fs.chmodSync(path.join(targetPath, "ludusavi"), 0o755);
|
||||
@@ -46,11 +58,79 @@ const downloadLudusavi = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const downloadAria2WindowsAndLinux = async () => {
|
||||
const file =
|
||||
process.platform === "win32"
|
||||
? "aria2-1.37.0-win-64bit-build1.zip"
|
||||
: "aria2-1.37.0-1-x86_64.pkg.tar.zst";
|
||||
|
||||
const downloadUrl =
|
||||
process.platform === "win32"
|
||||
? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
|
||||
: "https://archlinux.org/packages/extra/x86_64/aria2/download/";
|
||||
|
||||
console.log(`Downloading ${file}...`);
|
||||
|
||||
const response = await axios.get(downloadUrl, { responseType: "stream" });
|
||||
|
||||
const stream = response.data.pipe(fs.createWriteStream(file));
|
||||
|
||||
stream.on("finish", async () => {
|
||||
console.log(`Downloaded ${file}, extracting...`);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
await exec(`npx extract-zip ${file}`);
|
||||
console.log("Extracted. Renaming folder...");
|
||||
|
||||
fs.mkdirSync("aria2");
|
||||
fs.copyFileSync(
|
||||
path.join(file.replace(".zip", ""), "aria2c.exe"),
|
||||
"aria2/aria2c.exe"
|
||||
);
|
||||
fs.rmSync(file.replace(".zip", ""), { recursive: true });
|
||||
} else {
|
||||
await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
|
||||
console.log("Extracted. Copying binary file...");
|
||||
fs.mkdirSync("aria2");
|
||||
fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
|
||||
fs.rmSync("usr", { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Extracted ${file}, removing compressed downloaded file...`);
|
||||
fs.rmSync(file);
|
||||
});
|
||||
};
|
||||
|
||||
const copyAria2Macos = async () => {
|
||||
console.log("Checking if aria2 is installed...");
|
||||
|
||||
const isAria2Installed = spawnSync("which", ["aria2c"]).status;
|
||||
|
||||
if (isAria2Installed != 0) {
|
||||
console.log("Please install aria2");
|
||||
console.log("brew install aria2");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Copying aria2 binary...");
|
||||
fs.mkdirSync("aria2");
|
||||
await exec(`cp $(which aria2c) aria2/aria2c`);
|
||||
};
|
||||
|
||||
const copyAria2 = () => {
|
||||
const aria2Path =
|
||||
process.platform === "win32" ? "aria2/aria2c.exe" : "aria2/aria2c";
|
||||
|
||||
if (fs.existsSync(aria2Path)) {
|
||||
console.log("Aria2 already exists, skipping download...");
|
||||
return;
|
||||
}
|
||||
if (process.platform == "darwin") {
|
||||
copyAria2Macos();
|
||||
} else {
|
||||
downloadAria2WindowsAndLinux();
|
||||
}
|
||||
};
|
||||
|
||||
copyAria2();
|
||||
downloadLudusavi();
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
const binariesPath = path.join(__dirname, "..", "binaries");
|
||||
|
||||
fs.chmodSync(path.join(binariesPath, "7zz"), 0o755);
|
||||
fs.chmodSync(path.join(binariesPath, "7zzs"), 0o755);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"options": "خيارات",
|
||||
"executable_section_title": "ملف التشغيل",
|
||||
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||
"downloads_secion_title": "التنزيلات",
|
||||
"downloads_section_title": "التنزيلات",
|
||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||
"danger_zone_section_title": "منطقة الخطر",
|
||||
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||
@@ -188,8 +188,8 @@
|
||||
"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_on_real_debrid": "هذا التنزيل غير متوفر على Real-Debrid وجلب حالة التنزيل من Real-Debrid غير متاح حاليًا.",
|
||||
"download_error_not_cached_on_torbox": "هذا التنزيل غير متوفر على TorBox وجلب حالة التنزيل من TorBox غير متاح حاليًا.",
|
||||
"game_removed_from_favorites": "تمت إزالة اللعبة من المفضلة",
|
||||
"game_added_to_favorites": "تمت إضافة اللعبة إلى المفضلة"
|
||||
},
|
||||
@@ -330,7 +330,7 @@
|
||||
"delete_theme_description": "سيؤدي هذا إلى حذف السمة {{theme}}",
|
||||
"cancel": "إلغاء",
|
||||
"appearance": "المظهر",
|
||||
"enable_torbox": "تفعيل Torbox",
|
||||
"enable_torbox": "تفعيل TorBox",
|
||||
"torbox_description": "TorBox هي خدمة seedbox متميزة تنافس أفضل الخوادم في السوق.",
|
||||
"torbox_account_linked": "تم ربط حساب TorBox",
|
||||
"real_debrid_account_linked": "تم ربط حساب Real-Debrid",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"options": "Опции",
|
||||
"executable_section_title": "Стартиращ файл",
|
||||
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Пускане\"",
|
||||
"downloads_secion_title": "Свалени",
|
||||
"downloads_section_title": "Свалени",
|
||||
"downloads_section_description": "Вижте актуализации или други версии на тази игра",
|
||||
"danger_zone_section_title": "Опасна зона",
|
||||
"danger_zone_section_description": "Премахнете тази игра от библиотеката си или от файловете, изтеглени от Hydra",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"options": "Opcions",
|
||||
"executable_section_title": "Executable",
|
||||
"executable_section_description": "Directori del fitxer des d'on s'executarà quan es cliqui a \"Executar\"",
|
||||
"downloads_secion_title": "Descàrregues",
|
||||
"downloads_section_title": "Descàrregues",
|
||||
"downloads_section_description": "Comprova actualitzacions o altres versions del videojoc",
|
||||
"danger_zone_section_title": "Zona de perill",
|
||||
"danger_zone_section_description": "Elimina aquest videojoc del teu catàleg o els fitxers descarregats per Hydra",
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"options": "Možnosti",
|
||||
"executable_section_title": "Spustitelné",
|
||||
"executable_section_description": "Umístění souboru který bude spuštěn při kliknutí na \"Hrát\"",
|
||||
"downloads_secion_title": "Stažené soubory",
|
||||
"downloads_section_title": "Stažené soubory",
|
||||
"downloads_section_description": "Zkontrolovat jestli není nová / odlišná verze hry",
|
||||
"danger_zone_section_title": "Nebezpečná zóna",
|
||||
"danger_zone_section_description": "Odebrat hru z knihovny / soubory stažené Hydrou",
|
||||
@@ -193,8 +193,8 @@
|
||||
"reset_achievements_error": "Nepodařilo se resetovat achievementy",
|
||||
"download_error_gofile_quota_exceeded": "Překročili jste vaši měsíční GoFile kvótu. Prosím vyčkejte na resetování kvóty.",
|
||||
"download_error_real_debrid_account_not_authorized": "Váš Real-Debrid účet není autorizován pro vytváření nových stahování. Prosím zkontrolujte nastavení vašeho účtu a zkuste to znovu.",
|
||||
"download_error_not_cached_in_real_debrid": "Toto stahování není dostupné na Real-Debrid a získávání informací o stahování z Real-Debrid není zatím dostupné.",
|
||||
"download_error_not_cached_in_torbox": "Toto stahování není dostupné na Torbox a získávání informací o stahování z Torbox není zatím dostupné.",
|
||||
"download_error_not_cached_on_real_debrid": "Toto stahování není dostupné na Real-Debrid a získávání informací o stahování z Real-Debrid není zatím dostupné.",
|
||||
"download_error_not_cached_on_torbox": "Toto stahování není dostupné na TorBox a získávání informací o stahování z TorBox není zatím dostupné.",
|
||||
"game_removed_from_favorites": "Hra odebrána z oblíbených",
|
||||
"game_added_to_favorites": "Hra přidána do oblíbených",
|
||||
"automatically_extract_downloaded_files": "Automaticky rozbalit stažené soubory"
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"options": "Valgmuligheder",
|
||||
"executable_section_title": "Eksekverbar fil",
|
||||
"executable_section_description": "Sti til filen som skal bruges når \"Spil\" bliver klikket",
|
||||
"downloads_secion_title": "Downloads",
|
||||
"downloads_section_title": "Downloads",
|
||||
"downloads_section_description": "Undersøg opdateringer eller andre versioner af dette spil",
|
||||
"danger_zone_section_title": "Farezonen",
|
||||
"danger_zone_section_description": "Fjern dette spil fra dit bibliotek eller filerne der er blevet downloadet af Hydra",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"options": "Optionen",
|
||||
"executable_section_title": "Ausführbare Datei",
|
||||
"executable_section_description": "Pfad der Datei, die bei Klick auf \"Play\" ausgeführt wird",
|
||||
"downloads_secion_title": "Downloads",
|
||||
"downloads_section_title": "Downloads",
|
||||
"downloads_section_description": "Sieh dir Updates oder andere Versionen dieses Spiels an",
|
||||
"danger_zone_section_title": "Gefahrenzone",
|
||||
"danger_zone_section_description": "Entferne dieses Spiel aus deiner Bibliothek oder die von Hydra heruntergeladenen Dateien",
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"options": "Options",
|
||||
"executable_section_title": "Executable",
|
||||
"executable_section_description": "Path of the file that will be executed when \"Play\" is clicked",
|
||||
"downloads_secion_title": "Downloads",
|
||||
"downloads_section_title": "Downloads",
|
||||
"downloads_section_description": "Check out updates or other versions of this game",
|
||||
"danger_zone_section_title": "Danger zone",
|
||||
"danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra",
|
||||
@@ -193,8 +193,9 @@
|
||||
"reset_achievements_error": "Failed to reset achievements",
|
||||
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||
"download_error_not_cached_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.",
|
||||
"download_error_not_cached_on_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||
"download_error_not_cached_on_torbox": "This download is not available on TorBox and polling download status from TorBox is not yet available.",
|
||||
"download_error_not_cached_on_hydra": "This download is not available on Nimbus.",
|
||||
"game_removed_from_favorites": "Game removed from favorites",
|
||||
"game_added_to_favorites": "Game added to favorites",
|
||||
"automatically_extract_downloaded_files": "Automatically extract downloaded files"
|
||||
@@ -338,7 +339,7 @@
|
||||
"delete_theme_description": "This will delete the theme {{theme}}",
|
||||
"cancel": "Cancel",
|
||||
"appearance": "Appearance",
|
||||
"enable_torbox": "Enable Torbox",
|
||||
"enable_torbox": "Enable TorBox",
|
||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||
"torbox_account_linked": "TorBox account linked",
|
||||
"create_real_debrid_account": "Click here if you don't have a Real-Debrid account yet",
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
"downloading_metadata": "Descargando metadatos de {{title}}…",
|
||||
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Calculando tiempo restante…",
|
||||
"installation_complete": "Instalación completada",
|
||||
"installation_complete_message": "Common redistributables instalados exitosamente",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)"
|
||||
},
|
||||
"catalogue": {
|
||||
@@ -59,6 +62,8 @@
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Ver opciones de descargas",
|
||||
"automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados",
|
||||
"download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.",
|
||||
"download_options_zero": "No hay opciones de descargas disponibles",
|
||||
"download_options_one": "{{count}} opción de descarga",
|
||||
"download_options_other": "{{count}} opciones de descargas",
|
||||
@@ -119,7 +124,7 @@
|
||||
"options": "Opciones",
|
||||
"executable_section_title": "Ejecutable",
|
||||
"executable_section_description": "Ruta del archivo que se ejecutará cuando se presione \"Jugar\"",
|
||||
"downloads_secion_title": "Descargas",
|
||||
"downloads_section_title": "Descargas",
|
||||
"downloads_section_description": "Buscar actualizaciones u otras versiones de este juego",
|
||||
"danger_zone_section_title": "Opciones Avanzadas",
|
||||
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)",
|
||||
@@ -190,8 +195,8 @@
|
||||
"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.",
|
||||
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
||||
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y el estado de descarga del sondeo aún no está disponible.",
|
||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||
"game_removed_from_favorites": "Juego removido de favoritos"
|
||||
},
|
||||
@@ -230,10 +235,19 @@
|
||||
"seeding": "Seeding",
|
||||
"stop_seeding": "Detener seeding",
|
||||
"resume_seeding": "Continuar seeding",
|
||||
"extract": "Extraer archivos",
|
||||
"extracting": "Extrayendo archivos…",
|
||||
"options": "Gestionar"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Ruta de descarga",
|
||||
"common_redist": "Common redistributables",
|
||||
"common_redist_description": "Las Common redistributables son requeridos para ejecutar algunos juegos. Es recomendado instalarlos para evitar problemas.",
|
||||
"create_real_debrid_account": "Presiona acá si no tienes una cuenta de Real-Debrid aún",
|
||||
"create_torbox_account": "Presiona acá si no tienes una cuenta de TorBox aún",
|
||||
"install_common_redist": "Instalar",
|
||||
"installing_common_redist": "Instalando…",
|
||||
"show_download_speed_in_megabytes": "Mostrar velocidad de descargar en megabytes por segundo",
|
||||
"change": "Cambiar",
|
||||
"notifications": "Notificaciones",
|
||||
"enable_download_notifications": "Cuando se completa una descarga",
|
||||
@@ -326,7 +340,7 @@
|
||||
"editor_tab_code": "Código",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Guardar",
|
||||
"enable_torbox": "Habilitar Torbox",
|
||||
"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",
|
||||
@@ -346,6 +360,8 @@
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Descarga completada",
|
||||
"extraction_complete": "Extracción completada",
|
||||
"game_extracted": "{{title}} extraído exitosamente",
|
||||
"game_ready_to_install": "{{title}} está listo para instalarse",
|
||||
"repack_list_updated": "Lista de repacks actualizadas",
|
||||
"repack_count_one": "{{count}} repack ha sido añadido",
|
||||
@@ -475,6 +491,7 @@
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Suscripción Hydra Cloud",
|
||||
"debrid_description": "Descargas hasta x4 más rápidas con Nimbus",
|
||||
"subscribe_now": "Suscribirse ahora",
|
||||
"cloud_saving": "Guardado en la nube",
|
||||
"cloud_achievements": "Guarda tus logros en la nube",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"options": "Valikud",
|
||||
"executable_section_title": "Käivitusfail",
|
||||
"executable_section_description": "Faili tee, mida käivitatakse \"Mängi\" nupule vajutades",
|
||||
"downloads_secion_title": "Allalaadimised",
|
||||
"downloads_section_title": "Allalaadimised",
|
||||
"downloads_section_description": "Vaata uuendusi või selle mängu teisi versioone",
|
||||
"danger_zone_section_title": "Ohutsoon",
|
||||
"danger_zone_section_description": "Eemalda see mäng oma kogust või Hydra poolt allalaaditud failid",
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
{
|
||||
"language_name": "Français",
|
||||
"app": {
|
||||
"successfully_signed_in": "Connecté avec succès"
|
||||
},
|
||||
"home": {
|
||||
"featured": "En vedette",
|
||||
"surprise_me": "Surprenez-moi",
|
||||
"no_results": "Aucun résultat trouvé"
|
||||
"no_results": "Aucun résultat trouvé",
|
||||
"start_typing": "Commencez à taper pour rechercher...",
|
||||
"hot": "Tendance",
|
||||
"weekly": "📅 Meilleurs jeux de la semaine",
|
||||
"achievements": "🏆 Jeux à terminer"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Catalogue",
|
||||
@@ -12,24 +19,46 @@
|
||||
"my_library": "Ma bibliothèque",
|
||||
"downloading_metadata": "{{title}} (Téléchargement des métadonnées…)",
|
||||
"paused": "{{title}} (En pause)",
|
||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||
"downloading": "{{title}} ({{percentage}} - Téléchargement…)",
|
||||
"filter": "Filtrer la bibliothèque",
|
||||
"home": "Page d’accueil",
|
||||
"home": "Page d'accueil",
|
||||
"queued": "{{title}} (En file d'attente)",
|
||||
"game_has_no_executable": "Aucun exécutable sélectionné pour ce jeu",
|
||||
"sign_in": "Se connecter",
|
||||
"friends": "Amis",
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"favorites": "Favoris"
|
||||
},
|
||||
"header": {
|
||||
"search": "Recherche",
|
||||
|
||||
"search": "Rechercher",
|
||||
"home": "Accueil",
|
||||
"catalogue": "Catalogue",
|
||||
"downloads": "Téléchargements",
|
||||
"search_results": "Résultats de la recherche",
|
||||
"settings": "Paramètres",
|
||||
"home": "Accueil"
|
||||
"version_available_install": "Version {{version}} disponible. Cliquez ici pour redémarrer et installer.",
|
||||
"version_available_download": "Version {{version}} disponible. Cliquez ici pour télécharger."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Aucun téléchargement en cours",
|
||||
"downloading_metadata": "Téléchargement des métadonnées de {{title}}…",
|
||||
"downloading": "Téléchargement de {{title}}… ({{percentage}} terminé) - Fin dans {{eta}} - {{speed}}"
|
||||
"downloading": "Téléchargement de {{title}}… ({{percentage}} terminé) - Fin dans {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Téléchargement de {{title}}… ({{percentage}} terminé) - Calcul du temps restant…",
|
||||
"checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Installation terminée",
|
||||
"installation_complete_message": "Redistribuables communs installés avec succès"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filtrer…",
|
||||
"developers": "Développeurs",
|
||||
"genres": "Genres",
|
||||
"tags": "Tags",
|
||||
"publishers": "Éditeurs",
|
||||
"download_sources": "Sources de téléchargement",
|
||||
"result_count": "{{resultCount}} résultats",
|
||||
"filter_count": "{{filterCount}} disponibles",
|
||||
"clear_filters": "Effacer {{filterCount}} sélectionnés"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Ouvrir les options de téléchargement",
|
||||
@@ -37,36 +66,139 @@
|
||||
"download_options_one": "{{count}} option de téléchargement",
|
||||
"download_options_other": "{{count}} options de téléchargement",
|
||||
"updated_at": "Mis à jour le {{updated_at}}",
|
||||
"install": "Installer",
|
||||
"resume": "Reprendre",
|
||||
"pause": "Pause",
|
||||
"cancel": "Annuler",
|
||||
"remove": "Supprimer",
|
||||
"space_left_on_disk": "{{space}} restant sur le disque",
|
||||
"space_left_on_disk": "{{space}} restants sur le disque",
|
||||
"eta": "Fin dans {{eta}}",
|
||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
||||
"calculating_eta": "Calcul du temps restant…",
|
||||
"downloading_metadata": "Téléchargement des métadonnées…",
|
||||
"filter": "Filtrer les repacks",
|
||||
"requirements": "Configuration requise",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Recommandée",
|
||||
"paused": "En pause",
|
||||
"release_date": "Sorti le {{date}}",
|
||||
"publisher": "Édité par {{publisher}}",
|
||||
"publisher": "Publié par {{publisher}}",
|
||||
"hours": "heures",
|
||||
"minutes": "minutes",
|
||||
"amount_hours": "{{amount}} heures",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"accuracy": "{{accuracy}}% précision",
|
||||
"add_to_library": "Ajouter à la bibliothèque",
|
||||
"remove_from_library": "Supprimer de la bibliothèque",
|
||||
"remove_from_library": "Retirer de la bibliothèque",
|
||||
"no_downloads": "Aucun téléchargement disponible",
|
||||
"next_suggestion": "Suggestion suivante",
|
||||
"play_time": "Joué pour {{amount}}",
|
||||
"install": "Installer",
|
||||
"play": "Jouer",
|
||||
"play_time": "{{amount}} de temps de jeu",
|
||||
"last_time_played": "Dernière partie {{period}}",
|
||||
"not_played_yet": "Vous n'avez pas encore joué à {{title}}",
|
||||
"next_suggestion": "Suggestion suivante",
|
||||
"play": "Jouer",
|
||||
"deleting": "Suppression de l'installateur…",
|
||||
"close": "Fermer",
|
||||
"deleting": "Suppression du programme d'installation…",
|
||||
"playing_now": "Jeu en cours",
|
||||
"last_time_played": "Dernièrement joué {{period}}"
|
||||
"playing_now": "En cours de jeu",
|
||||
"change": "Changer",
|
||||
"repacks_modal_description": "Choisissez le repack que vous souhaitez télécharger",
|
||||
"select_folder_hint": "Pour changer le dossier par défaut, allez dans les <0>Paramètres</0>",
|
||||
"download_now": "Télécharger maintenant",
|
||||
"no_shop_details": "Impossible d'obtenir les détails du magasin.",
|
||||
"download_options": "Options de téléchargement",
|
||||
"download_path": "Emplacement de téléchargement",
|
||||
"previous_screenshot": "Capture précédente",
|
||||
"next_screenshot": "Capture suivante",
|
||||
"screenshot": "Capture d'écran {{number}}",
|
||||
"open_screenshot": "Ouvrir la capture {{number}}",
|
||||
"download_settings": "Paramètres de téléchargement",
|
||||
"downloader": "Téléchargeur",
|
||||
"select_executable": "Sélectionner",
|
||||
"no_executable_selected": "Aucun exécutable sélectionné",
|
||||
"open_folder": "Ouvrir le dossier",
|
||||
"open_download_location": "Voir les fichiers téléchargés",
|
||||
"create_shortcut": "Créer un raccourci sur le bureau",
|
||||
"clear": "Effacer",
|
||||
"remove_files": "Supprimer les fichiers",
|
||||
"remove_from_library_title": "Êtes-vous sûr ?",
|
||||
"remove_from_library_description": "Ceci supprimera {{game}} de votre bibliothèque",
|
||||
"options": "Options",
|
||||
"executable_section_title": "Exécutable",
|
||||
"executable_section_description": "Chemin du fichier lancé quand \"Jouer\" est cliqué",
|
||||
"downloads_section_title": "Téléchargements",
|
||||
"downloads_section_description": "Découvrez les mises à jour ou autres versions de ce jeu",
|
||||
"danger_zone_section_title": "Zone de danger",
|
||||
"danger_zone_section_description": "Supprimez ce jeu de votre bibliothèque ou les fichiers téléchargés par Hydra",
|
||||
"download_in_progress": "Téléchargement en cours",
|
||||
"download_paused": "Téléchargement en pause",
|
||||
"last_downloaded_option": "Dernière option téléchargée",
|
||||
"create_shortcut_success": "Raccourci créé avec succès",
|
||||
"create_shortcut_error": "Erreur lors de la création du raccourci",
|
||||
"nsfw_content_title": "Ce jeu contient du contenu inapproprié",
|
||||
"nsfw_content_description": "{{title}} contient du contenu pouvant ne pas convenir à tous les âges. Voulez-vous continuer ?",
|
||||
"allow_nsfw_content": "Continuer",
|
||||
"refuse_nsfw_content": "Retourner",
|
||||
"stats": "Statistiques",
|
||||
"download_count": "Téléchargements",
|
||||
"player_count": "Joueurs actifs",
|
||||
"download_error": "Cette option de téléchargement n'est pas disponible",
|
||||
"download": "Télécharger",
|
||||
"executable_path_in_use": "Exécutable déjà utilisé par \"{{game}}\"",
|
||||
"warning": "Attention :",
|
||||
"hydra_needs_to_remain_open": "Pour ce téléchargement, Hydra doit rester ouvert jusqu'à la fin. Si Hydra se ferme avant, la progression sera perdue.",
|
||||
"achievements": "Succès",
|
||||
"achievements_count": "Succès {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Sauvegarde Cloud",
|
||||
"cloud_save_description": "Sauvegardez vos progrès dans le cloud et continuez à jouer sur n'importe quel appareil",
|
||||
"backups": "Sauvegardes",
|
||||
"install_backup": "Restaurer",
|
||||
"delete_backup": "Supprimer",
|
||||
"create_backup": "Nouvelle sauvegarde",
|
||||
"last_backup_date": "Dernière sauvegarde le {{date}}",
|
||||
"no_backup_preview": "Aucune sauvegarde trouvée pour ce titre",
|
||||
"restoring_backup": "Restauration de la sauvegarde ({{progress}} terminé)…",
|
||||
"uploading_backup": "Envoi de la sauvegarde…",
|
||||
"no_backups": "Vous n'avez pas encore créé de sauvegarde pour ce jeu",
|
||||
"backup_uploaded": "Sauvegarde envoyée",
|
||||
"backup_deleted": "Sauvegarde supprimée",
|
||||
"backup_restored": "Sauvegarde restaurée",
|
||||
"see_all_achievements": "Voir tous les succès",
|
||||
"sign_in_to_see_achievements": "Connectez-vous pour voir les succès",
|
||||
"mapping_method_automatic": "Automatique",
|
||||
"mapping_method_manual": "Manuel",
|
||||
"mapping_method_label": "Méthode de mappage",
|
||||
"files_automatically_mapped": "Fichiers mappés automatiquement",
|
||||
"no_backups_created": "Aucune sauvegarde créée pour ce jeu",
|
||||
"manage_files": "Gérer les fichiers",
|
||||
"loading_save_preview": "Recherche de jeux sauvegardés…",
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Le préfixe Wine utilisé pour lancer ce jeu",
|
||||
"launch_options": "Options de lancement",
|
||||
"launch_options_description": "Les utilisateurs avancés peuvent modifier les options de lancement (fonction expérimentale)",
|
||||
"launch_options_placeholder": "Aucun paramètre spécifié",
|
||||
"no_download_option_info": "Pas d'information disponible",
|
||||
"backup_deletion_failed": "Échec de la suppression de la sauvegarde",
|
||||
"max_number_of_artifacts_reached": "Nombre maximal de sauvegardes atteint pour ce jeu",
|
||||
"achievements_not_sync": "Voir comment synchroniser vos succès",
|
||||
"manage_files_description": "Gérer les fichiers qui seront sauvegardés et restaurés",
|
||||
"select_folder": "Sélectionner un dossier",
|
||||
"backup_from": "Sauvegarde du {{date}}",
|
||||
"automatic_backup_from": "Sauvegarde automatique du {{date}}",
|
||||
"enable_automatic_cloud_sync": "Activer la synchronisation cloud automatique",
|
||||
"custom_backup_location_set": "Emplacement de sauvegarde personnalisé défini",
|
||||
"no_directory_selected": "Aucun dossier sélectionné",
|
||||
"no_write_permission": "Impossible de télécharger dans ce dossier. Cliquez ici pour en savoir plus.",
|
||||
"reset_achievements": "Réinitialiser les succès",
|
||||
"reset_achievements_description": "Ceci réinitialisera tous les succès pour {{game}}",
|
||||
"reset_achievements_title": "Êtes-vous sûr ?",
|
||||
"reset_achievements_success": "Succès réinitialisés avec succès",
|
||||
"reset_achievements_error": "Échec de la réinitialisation des succès",
|
||||
"download_error_gofile_quota_exceeded": "Vous avez dépassé votre quota mensuel Gofile. Attendez la remise à zéro du quota.",
|
||||
"download_error_real_debrid_account_not_authorized": "Votre compte Real-Debrid n'est pas autorisé à effectuer de nouveaux téléchargements. Veuillez vérifier les paramètres de votre compte et réessayer.",
|
||||
"download_error_not_cached_on_real_debrid": "Ce téléchargement n'est pas disponible sur Real-Debrid, et le suivi n'est pas encore disponible.",
|
||||
"download_error_not_cached_on_torbox": "Ce téléchargement n'est pas disponible sur TorBox, et le suivi n'est pas encore disponible.",
|
||||
"download_error_not_cached_on_hydra": "Ce téléchargement n'est pas disponible sur Nimbus.",
|
||||
"game_removed_from_favorites": "Jeu retiré des favoris",
|
||||
"game_added_to_favorites": "Jeu ajouté aux favoris",
|
||||
"automatically_extract_downloaded_files": "Extraire automatiquement les fichiers téléchargés"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activer Hydra",
|
||||
@@ -83,51 +215,292 @@
|
||||
"paused": "En pause",
|
||||
"verifying": "Vérification en cours…",
|
||||
"completed": "Terminé",
|
||||
"removed": "Non téléchargé",
|
||||
"cancel": "Annuler",
|
||||
"filter": "Filtrer les jeux téléchargés",
|
||||
"remove": "Supprimer",
|
||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
||||
"delete": "Supprimer le programme d'installation",
|
||||
"delete_modal_description": "Cela supprimera tous les fichiers d'installation de votre ordinateur",
|
||||
"delete_modal_title": "Es-tu sûr?",
|
||||
"deleting": "Suppression du programme d'installation…",
|
||||
"install": "Installer"
|
||||
"delete": "Supprimer le programme d'installation",
|
||||
"delete_modal_title": "Êtes-vous sûr ?",
|
||||
"delete_modal_description": "Cela supprimera tous les fichiers d'installation de votre ordinateur",
|
||||
"install": "Installer",
|
||||
"download_in_progress": "En cours",
|
||||
"queued_downloads": "Téléchargements en attente",
|
||||
"downloads_completed": "Terminés",
|
||||
"queued": "En attente",
|
||||
"no_downloads_title": "Tellement vide",
|
||||
"no_downloads_description": "Vous n'avez encore rien téléchargé avec Hydra, mais il n'est jamais trop tard pour commencer.",
|
||||
"checking_files": "Vérification des fichiers…",
|
||||
"seeding": "Partage",
|
||||
"stop_seeding": "Arrêter le partage",
|
||||
"resume_seeding": "Reprendre le partage",
|
||||
"options": "Gérer",
|
||||
"extract": "Extraire les fichiers",
|
||||
"extracting": "Extraction des fichiers…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Chemin des téléchargements",
|
||||
"change": "Mettre à jour",
|
||||
"notifications": "Notifications",
|
||||
"enable_download_notifications": "Quand un téléchargement est terminé",
|
||||
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
|
||||
"enable_download_notifications": "Lorsqu'un téléchargement est terminé",
|
||||
"enable_repack_list_notifications": "Lorsqu'un nouveau repack est ajouté",
|
||||
"real_debrid_api_token_label": "Jeton API Real-Debrid",
|
||||
"quit_app_instead_hiding": "Ne pas masquer Hydra à la fermeture",
|
||||
"launch_with_system": "Lancer Hydra au démarrage du système",
|
||||
"general": "Général",
|
||||
"behavior": "Comportement",
|
||||
"download_sources": "Sources de téléchargement",
|
||||
"language": "Langue",
|
||||
"api_token": "Jeton API",
|
||||
"enable_real_debrid": "Activer Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid est un téléchargeur sans restriction qui vous permet de télécharger rapidement des fichiers, uniquement limités par votre vitesse Internet.",
|
||||
"debrid_invalid_token": "Jeton API invalide",
|
||||
"debrid_api_token_hint": "Vous pouvez obtenir votre jeton API <0>ici</0>",
|
||||
"real_debrid_free_account_error": "Le compte \"{{username}}\" est un compte gratuit. Veuillez vous abonner à Real-Debrid",
|
||||
"debrid_linked_message": "Compte \"{{username}}\" lié",
|
||||
"save_changes": "Enregistrer les modifications",
|
||||
"changes_saved": "Modifications enregistrées avec succès",
|
||||
"download_sources_description": "Hydra récupère les liens de téléchargement à partir de ces sources. L'URL source doit être un lien direct vers un fichier .json contenant les liens de téléchargement.",
|
||||
"validate_download_source": "Valider",
|
||||
"remove_download_source": "Supprimer",
|
||||
"add_download_source": "Ajouter une source",
|
||||
"download_count_zero": "Aucune option de téléchargement",
|
||||
"download_count_one": "{{countFormatted}} option de téléchargement",
|
||||
"download_count_other": "{{countFormatted}} options de téléchargement",
|
||||
"download_source_url": "URL de la source",
|
||||
"add_download_source_description": "Insérez l'URL du fichier .json",
|
||||
"download_source_up_to_date": "À jour",
|
||||
"download_source_errored": "Erreur",
|
||||
"sync_download_sources": "Synchroniser les sources",
|
||||
"removed_download_source": "Source de téléchargement supprimée",
|
||||
"removed_download_sources": "Sources de téléchargement supprimées",
|
||||
"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"
|
||||
"description_confirmation_delete_all_sources": "Vous supprimerez toutes les sources de téléchargement",
|
||||
"button_delete_all_sources": "Tout supprimer",
|
||||
"added_download_source": "Source de téléchargement ajoutée",
|
||||
"download_sources_synced": "Toutes les sources de téléchargement sont synchronisées",
|
||||
"insert_valid_json_url": "Insérez une URL JSON valide",
|
||||
"found_download_option_zero": "Aucune option de téléchargement trouvée",
|
||||
"found_download_option_one": "{{countFormatted}} option trouvée",
|
||||
"found_download_option_other": "{{countFormatted}} options trouvées",
|
||||
"import": "Importer",
|
||||
"public": "Publique",
|
||||
"private": "Privé",
|
||||
"friends_only": "Amis uniquement",
|
||||
"privacy": "Confidentialité",
|
||||
"profile_visibility": "Visibilité du profil",
|
||||
"profile_visibility_description": "Choisissez qui peut voir votre profil et bibliothèque",
|
||||
"required_field": "Ce champ est requis",
|
||||
"source_already_exists": "Cette source a déjà été ajoutée",
|
||||
"must_be_valid_url": "La source doit être une URL valide",
|
||||
"blocked_users": "Utilisateurs bloqués",
|
||||
"user_unblocked": "Utilisateur débloqué",
|
||||
"enable_achievement_notifications": "Quand un succès est débloqué",
|
||||
"launch_minimized": "Lancer Hydra minimisé",
|
||||
"disable_nsfw_alert": "Désactiver l'alerte NSFW",
|
||||
"seed_after_download_complete": "Partager après téléchargement",
|
||||
"show_hidden_achievement_description": "Afficher la description des succès cachés avant de les débloquer",
|
||||
"account": "Compte",
|
||||
"no_users_blocked": "Aucun utilisateur bloqué",
|
||||
"subscription_active_until": "Votre Hydra Cloud est actif jusqu'au {{date}}",
|
||||
"manage_subscription": "Gérer l'abonnement",
|
||||
"update_email": "Modifier l'email",
|
||||
"update_password": "Modifier le mot de passe",
|
||||
"current_email": "Email actuel :",
|
||||
"no_email_account": "Vous n'avez pas encore défini d'email",
|
||||
"account_data_updated_successfully": "Informations du compte mises à jour",
|
||||
"renew_subscription": "Renouveler Hydra Cloud",
|
||||
"subscription_expired_at": "Votre abonnement a expiré le {{date}}",
|
||||
"no_subscription": "Profitez d'Hydra de la meilleure façon possible",
|
||||
"become_subscriber": "Devenir membre Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Le renouvellement automatique est désactivé",
|
||||
"subscription_renews_on": "Votre abonnement sera renouvelé le {{date}}",
|
||||
"bill_sent_until": "Votre prochaine facture sera envoyée à cette date",
|
||||
"no_themes": "Vous n'avez pas encore de thèmes, cliquez ici pour créer votre premier chef-d'œuvre.",
|
||||
"editor_tab_code": "Code",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Enregistrer",
|
||||
"web_store": "Web store",
|
||||
"clear_themes": "Effacer",
|
||||
"create_theme": "Créer",
|
||||
"create_theme_modal_title": "Créer un thème personnalisé",
|
||||
"create_theme_modal_description": "Créer un nouveau thème pour personnaliser l'apparence d'Hydra",
|
||||
"theme_name": "Nom du thème",
|
||||
"insert_theme_name": "Entrez le nom du thème",
|
||||
"set_theme": "Activer le thème",
|
||||
"unset_theme": "Désactiver le thème",
|
||||
"delete_theme": "Supprimer le thème",
|
||||
"edit_theme": "Modifier le thème",
|
||||
"delete_all_themes": "Supprimer tous les thèmes",
|
||||
"delete_all_themes_description": "Cela supprimera tous vos thèmes personnalisés",
|
||||
"delete_theme_description": "Cela supprimera le thème {{theme}}",
|
||||
"cancel": "Annuler",
|
||||
"appearance": "Apparence",
|
||||
"enable_torbox": "Activer TorBox",
|
||||
"torbox_description": "TorBox est votre service de seedbox premium qui rivalise avec les meilleurs serveurs du marché.",
|
||||
"torbox_account_linked": "Compte TorBox lié",
|
||||
"create_real_debrid_account": "Cliquez ici pour créer un compte Real-Debrid",
|
||||
"create_torbox_account": "Cliquez ici pour créer un compte TorBox",
|
||||
"real_debrid_account_linked": "Compte Real-Debrid lié",
|
||||
"name_min_length": "Le nom du thème doit comporter au moins 3 caractères",
|
||||
"import_theme": "Importer un thème",
|
||||
"import_theme_description": "Vous allez importer {{theme}} du magasin de thèmes",
|
||||
"error_importing_theme": "Erreur lors de l'importation du thème",
|
||||
"theme_imported": "Thème importé avec succès",
|
||||
"enable_friend_request_notifications": "Lors de réception de demande d'ami",
|
||||
"enable_auto_install": "Télécharger les mises à jour automatiquement",
|
||||
"common_redist": "Redistribuables communs",
|
||||
"common_redist_description": "Certains jeux nécessitent les redistribuables communs. L'installation est recommandée.",
|
||||
"install_common_redist": "Installer",
|
||||
"installing_common_redist": "Installation…",
|
||||
"show_download_speed_in_megabytes": "Afficher la vitesse de téléchargement en mégaoctets par seconde"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Téléchargement terminé",
|
||||
"game_ready_to_install": "{{title}} est prêt à être installé",
|
||||
"repack_list_updated": "Liste de repacks mise à jour",
|
||||
"repack_count_one": "{{count}} repack ajouté",
|
||||
"repack_count_other": "{{count}} repacks ajoutés"
|
||||
"repack_count_other": "{{count}} repacks ajoutés",
|
||||
"new_update_available": "Version {{version}} disponible",
|
||||
"restart_to_install_update": "Redémarrez Hydra pour installer la mise à jour",
|
||||
"notification_achievement_unlocked_title": "Succès débloqué pour {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} et {{count}} autre(s) débloqués",
|
||||
"new_friend_request_description": "Vous avez reçu une nouvelle demande d'ami",
|
||||
"new_friend_request_title": "Nouvelle demande d'ami",
|
||||
"extraction_complete": "Extraction terminée",
|
||||
"game_extracted": "{{title}} extrait avec succès"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Ouvrir Hydra",
|
||||
"quit": "Quitter"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Disponible",
|
||||
"available_other": "Disponibles",
|
||||
"no_downloads": "Aucun téléchargement disponible"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programmes non installés",
|
||||
"description": "Les exécutables Wine ou Lutris sont introuvables sur votre système",
|
||||
"instructions": "Vérifiez la bonne façon d'installer l'un d'entre eux sur votre distribution Linux afin que le jeu puisse fonctionner normalement",
|
||||
"title": "Programmes non installés"
|
||||
"instructions": "Vérifiez la bonne façon d'installer l'un d'entre eux sur votre distribution Linux afin que le jeu puisse fonctionner normalement"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Page suivante",
|
||||
"previous_page": "Page précédente"
|
||||
"modal": {
|
||||
"close": "Fermer la fenêtre"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Afficher/Masquer le mot de passe"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} heures",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"last_time_played": "Dernière partie {{period}}",
|
||||
"activity": "Activité récente",
|
||||
"library": "Bibliothèque",
|
||||
"total_play_time": "Temps de jeu total",
|
||||
"no_recent_activity_title": "Hmm… rien ici",
|
||||
"no_recent_activity_description": "Vous n'avez pas joué récemment. Il est temps d'y remédier !",
|
||||
"display_name": "Nom d'affichage",
|
||||
"saving": "Enregistrement",
|
||||
"save": "Enregistrer",
|
||||
"edit_profile": "Modifier le profil",
|
||||
"saved_successfully": "Enregistré avec succès",
|
||||
"try_again": "Veuillez réessayer",
|
||||
"sign_out_modal_title": "Êtes-vous sûr ?",
|
||||
"cancel": "Annuler",
|
||||
"successfully_signed_out": "Déconnecté avec succès",
|
||||
"sign_out": "Se déconnecter",
|
||||
"playing_for": "En jeu depuis {{amount}}",
|
||||
"sign_out_modal_text": "Votre bibliothèque est liée à ce compte. Si vous vous déconnectez, elle ne sera plus visible et la progression ne sera pas sauvegardée. Continuer ?",
|
||||
"add_friends": "Ajouter des amis",
|
||||
"add": "Ajouter",
|
||||
"friend_code": "Code ami",
|
||||
"see_profile": "Voir le profil",
|
||||
"sending": "Envoi",
|
||||
"friend_request_sent": "Demande d'ami envoyée",
|
||||
"friends": "Amis",
|
||||
"friends_list": "Liste d'amis",
|
||||
"user_not_found": "Utilisateur introuvable",
|
||||
"block_user": "Bloquer l'utilisateur",
|
||||
"add_friend": "Ajouter un ami",
|
||||
"request_sent": "Demande envoyée",
|
||||
"request_received": "Demande reçue",
|
||||
"accept_request": "Accepter la demande",
|
||||
"ignore_request": "Ignorer la demande",
|
||||
"cancel_request": "Annuler la demande",
|
||||
"undo_friendship": "Retirer de la liste d'amis",
|
||||
"request_accepted": "Demande acceptée",
|
||||
"user_blocked_successfully": "Utilisateur bloqué avec succès",
|
||||
"user_block_modal_text": "Vous allez bloquer {{displayName}}",
|
||||
"blocked_users": "Utilisateurs bloqués",
|
||||
"unblock": "Débloquer",
|
||||
"no_friends_added": "Vous n'avez pas encore d'amis",
|
||||
"pending": "En attente",
|
||||
"no_pending_invites": "Aucune invitation en attente",
|
||||
"no_blocked_users": "Aucun utilisateur bloqué",
|
||||
"friend_code_copied": "Code ami copié",
|
||||
"undo_friendship_modal_text": "Vous allez retirer {{displayName}} de vos amis",
|
||||
"privacy_hint": "Pour changer qui voit ceci, allez dans les <0>Paramètres</0>",
|
||||
"locked_profile": "Ce profil est privé",
|
||||
"image_process_failure": "Erreur lors du traitement de l'image",
|
||||
"required_field": "Ce champ est requis",
|
||||
"displayname_min_length": "Le nom doit contenir au moins 3 caractères",
|
||||
"displayname_max_length": "Le nom doit contenir au maximum 50 caractères",
|
||||
"report_profile": "Signaler ce profil",
|
||||
"report_reason": "Pourquoi signaler ce profil ?",
|
||||
"report_description": "Informations supplémentaires",
|
||||
"report_description_placeholder": "Infos en plus",
|
||||
"report": "Signaler",
|
||||
"report_reason_hate": "Discours de haine",
|
||||
"report_reason_sexual_content": "Contenu sexuel",
|
||||
"report_reason_violence": "Violence",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Autre",
|
||||
"profile_reported": "Profil signalé",
|
||||
"your_friend_code": "Votre code ami :",
|
||||
"upload_banner": "Télécharger une bannière",
|
||||
"uploading_banner": "Téléversement de la bannière…",
|
||||
"background_image_updated": "Image de fond mise à jour",
|
||||
"stats": "Statistiques",
|
||||
"achievements": "Succès",
|
||||
"games": "Jeux",
|
||||
"top_percentile": "Top {{percentile}}%",
|
||||
"ranking_updated_weekly": "Classement mis à jour chaque semaine",
|
||||
"playing": "En train de jouer à {{game}}",
|
||||
"achievements_unlocked": "Succès débloqués",
|
||||
"earned_points": "Points gagnés",
|
||||
"show_achievements_on_profile": "Afficher vos succès sur votre profil",
|
||||
"show_points_on_profile": "Afficher vos points sur votre profil"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Succès débloqué",
|
||||
"user_achievements": "Succès de {{displayName}}",
|
||||
"your_achievements": "Vos succès",
|
||||
"unlocked_at": "Débloqué le : {{date}}",
|
||||
"subscription_needed": "Un abonnement Hydra Cloud est requis",
|
||||
"new_achievements_unlocked": "{{achievementCount}} nouveaux succès débloqués sur {{gameCount}} jeux",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} succès",
|
||||
"achievements_unlocked_for_game": "{{achievementCount}} nouveaux succès débloqués sur {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Ce succès est caché",
|
||||
"achievement_earn_points": "Gagnez {{points}} points avec ce succès",
|
||||
"earned_points": "Points gagnés :",
|
||||
"available_points": "Points disponibles :",
|
||||
"how_to_earn_achievements_points": "Comment gagner des points de succès ?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Abonnement Hydra Cloud",
|
||||
"subscribe_now": "S'abonner",
|
||||
"cloud_saving": "Sauvegarde Cloud",
|
||||
"cloud_achievements": "Sauvegardez vos succès dans le cloud",
|
||||
"animated_profile_picture": "Photo de profil animée",
|
||||
"premium_support": "Support premium",
|
||||
"show_and_compare_achievements": "Montrez et comparez vos succès avec d'autres utilisateurs",
|
||||
"animated_profile_banner": "Bannière de profil animée",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Vous avez découvert une fonctionnalité Hydra Cloud !",
|
||||
"learn_more": "En savoir plus",
|
||||
"debrid_description": "Téléchargez jusqu'à 4x plus vite avec Nimbus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"options": "Opsi",
|
||||
"executable_section_title": "Eksekusi",
|
||||
"executable_section_description": "Path file eksekusi saat \"Main\" diklik",
|
||||
"downloads_secion_title": "Unduhan",
|
||||
"downloads_section_title": "Unduhan",
|
||||
"downloads_section_description": "Cek update atau versi lain dari game ini",
|
||||
"danger_zone_section_title": "Zona Berbahaya",
|
||||
"danger_zone_section_description": "Hapus game ini dari perpustakaan kamu atau file yang diunduh oleh Hydra",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"options": "Параметрлер",
|
||||
"executable_section_title": "Файл",
|
||||
"executable_section_description": "\"Ойнау\" батырмасын басқанда іске қосылатын файл жолы",
|
||||
"downloads_secion_title": "Жүктеулер",
|
||||
"downloads_section_title": "Жүктеулер",
|
||||
"downloads_section_description": "Ойынның жаңартулары немесе басқа нұсқалары бар-жоғын тексеру",
|
||||
"danger_zone_section_title": "Қауіпті аймақ",
|
||||
"danger_zone_section_description": "Осы ойынды кітапханаңыздан жою немесе Hydra жүктеген файлдарды жою",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"options": "Valgmuligheter",
|
||||
"executable_section_title": "Kjørbar fil",
|
||||
"executable_section_description": "Sti til filen som skal brukes når det trykkes på \"Spill\"",
|
||||
"downloads_secion_title": "Nedlastinger",
|
||||
"downloads_section_title": "Nedlastinger",
|
||||
"downloads_section_description": "Sjekk for oppdateringer eller andre versjoner af dette spillet",
|
||||
"danger_zone_section_title": "Faresonen",
|
||||
"danger_zone_section_description": "Fjern dette spillet fra biblioteket ditt eller filene som har blitt lastet ned av Hydra",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"remove_from_library_title": "Tem certeza?",
|
||||
"executable_section_title": "Executável",
|
||||
"executable_section_description": "O caminho do arquivo que será executado ao clicar em \"Jogar\"",
|
||||
"downloads_secion_title": "Downloads",
|
||||
"downloads_section_title": "Downloads",
|
||||
"downloads_section_description": "Confira atualizações ou versões diferentes para este mesmo título",
|
||||
"danger_zone_section_title": "Zona de perigo",
|
||||
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra",
|
||||
@@ -182,8 +182,9 @@
|
||||
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais.",
|
||||
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
||||
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique 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.",
|
||||
"download_error_not_cached_on_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_on_torbox": "Este download não está disponível no TorBox e a verificação do status do download não está disponível.",
|
||||
"download_error_not_cached_on_hydra": "Este download não está disponível no Nimbus.",
|
||||
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos",
|
||||
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados"
|
||||
@@ -325,7 +326,7 @@
|
||||
"delete_theme_description": "Isso irá deletar o tema {{theme}}",
|
||||
"cancel": "Cancelar",
|
||||
"appearance": "Aparência",
|
||||
"enable_torbox": "Habilitar Torbox",
|
||||
"enable_torbox": "Habilitar TorBox",
|
||||
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||
"create_real_debrid_account": "Clique aqui se você ainda não tem uma conta do Real-Debrid",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"remove_from_library_title": "Tens a certeza?",
|
||||
"executable_section_title": "Executável",
|
||||
"executable_section_description": "O caminho do ficheiro que vai ser executado ao clicar em \"Jogar\"",
|
||||
"downloads_secion_title": "Transferências",
|
||||
"downloads_section_title": "Transferências",
|
||||
"downloads_section_description": "Encontra atualizações ou versões diferentes para este mesmo título",
|
||||
"danger_zone_section_title": "Zona de perigo",
|
||||
"danger_zone_section_description": "Remove o jogo da tua biblioteca ou os ficheiros que foram transferidos pelo Hydra",
|
||||
@@ -175,8 +175,8 @@
|
||||
"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.",
|
||||
"download_error_not_cached_on_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_on_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"
|
||||
},
|
||||
@@ -321,7 +321,7 @@
|
||||
"delete_theme_description": "Isto irá apagar o tema {{theme}}",
|
||||
"cancel": "Cancelar",
|
||||
"appearance": "Aparência",
|
||||
"enable_torbox": "Ativar Torbox",
|
||||
"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",
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"options": "Настройки",
|
||||
"executable_section_title": "Файл",
|
||||
"executable_section_description": "Путь к файлу, который будет запущен при нажатии на \"Play\"",
|
||||
"downloads_secion_title": "Загрузки",
|
||||
"downloads_section_title": "Загрузки",
|
||||
"downloads_section_description": "Проверить наличие обновлений или других версий игры",
|
||||
"danger_zone_section_title": "Опасная зона",
|
||||
"danger_zone_section_description": "Вы можете удалить эту игру из вашей библиотеки или файлы скачанные из Hydra",
|
||||
@@ -193,8 +193,8 @@
|
||||
"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_on_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
|
||||
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
|
||||
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||
"game_removed_from_favorites": "Игра удалена из избранного",
|
||||
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов"
|
||||
@@ -338,7 +338,7 @@
|
||||
"delete_theme_description": "Это приведет к удалению темы {{theme}}",
|
||||
"cancel": "Отменить",
|
||||
"appearance": "Внешний вид",
|
||||
"enable_torbox": "Включить Torbox",
|
||||
"enable_torbox": "Включить TorBox",
|
||||
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
|
||||
"torbox_account_linked": "Аккаунт TorBox привязан",
|
||||
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"options": "Seçenekler",
|
||||
"executable_section_title": "Çalıştırılabilir dosya",
|
||||
"executable_section_description": "\"Oyna\" butonuna tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"downloads_secion_title": "İndirmeler",
|
||||
"downloads_section_title": "İndirmeler",
|
||||
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
|
||||
"danger_zone_section_title": "Tehlike bölgesi",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden kaldırın veya Hydra tarafından indirilen dosyaları silin.",
|
||||
@@ -193,8 +193,9 @@
|
||||
"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.",
|
||||
"download_error_not_cached_on_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve Real-Debrid'den indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_on_torbox": "Bu indirme TorBox'ta mevcut değil ve TorBox'tan indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_on_hydra": "Bu indirme Nimbus'ta mevcut değil.",
|
||||
"game_removed_from_favorites": "Oyun favorilerden silindi",
|
||||
"game_added_to_favorites": "Oyun favorilere eklendi",
|
||||
"automatically_extract_downloaded_files": "Yüklenmiş dosyaları otomatik olarak çıkart"
|
||||
@@ -338,7 +339,7 @@
|
||||
"delete_theme_description": "Bu {{theme}} temasını silecektir",
|
||||
"cancel": "İptal",
|
||||
"appearance": "Görünüm",
|
||||
"enable_torbox": "Torbox'u etkinleştir",
|
||||
"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ğlandı",
|
||||
"create_real_debrid_account": "Henüz bir Real-Debrid hesabınız yoksa buraya tıklayın",
|
||||
@@ -354,7 +355,8 @@
|
||||
"common_redist": "Ortak bağımlılıklar",
|
||||
"common_redist_description": "Bazı oyunların çalışabilmesi için genel bağımlılıklar gereklidir. Sorun yaşamamak için bunların yüklenmesi önerilir.",
|
||||
"install_common_redist": "Yükle",
|
||||
"installing_common_redist": "Yükleniyor…"
|
||||
"installing_common_redist": "Yükleniyor…",
|
||||
"show_download_speed_in_megabytes": "İndirme hızını megabayt/saniye (MB/s) cinsinden göster"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "İndirme tamamlandı",
|
||||
@@ -498,6 +500,7 @@
|
||||
"animated_profile_banner": "Animasyonlu profil afişi",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
|
||||
"learn_more": "Daha Fazla Bilgi Edinin"
|
||||
"learn_more": "Daha Fazla Bilgi Edinin",
|
||||
"debrid_description": "Nimbus ile 4 kata kadar daha hızlı indirin"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
"home": {
|
||||
"featured": "Рекомендоване",
|
||||
"surprise_me": "Здивуй мене",
|
||||
"no_results": "Результатів не знайдено"
|
||||
"no_results": "Результатів не знайдено",
|
||||
"start_typing": "Почніть набирати текст для пошуку...",
|
||||
"hot": "Гарячі новинки",
|
||||
"weekly": "📅 Найкращі ігри цього тижня",
|
||||
"achievements": "🏆 Ігри на проходження"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Каталог",
|
||||
@@ -21,11 +25,12 @@
|
||||
"game_has_no_executable": "Не було вибрано файл для запуску гри",
|
||||
"queued": "{{title}} в черзі",
|
||||
"sign_in": "Увійти",
|
||||
"favorites": "Улюблені"
|
||||
"favorites": "Улюблені",
|
||||
"friends": "Друзі",
|
||||
"need_help": "Потрібна допомога?"
|
||||
},
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
|
||||
"home": "Головна",
|
||||
"catalogue": "Каталог",
|
||||
"downloads": "Завантаження",
|
||||
@@ -38,11 +43,22 @@
|
||||
"no_downloads_in_progress": "Немає активних завантажень",
|
||||
"downloading_metadata": "Завантаження метаданих {{title}}…",
|
||||
"downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Завантаження {{title}}… ({{percentage}} завершено) - Обчислення залишкового часу…"
|
||||
"calculating_eta": "Завантаження {{title}}… ({{percentage}} завершено) - Обчислення залишкового часу…",
|
||||
"checking_files": "Перевірка файлів {{title}} ({{percentage}} виповнено)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Встановлення завершено",
|
||||
"installation_complete_message": "Загальні розповсюджувані файли успішно встановлено"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Наступна сторінка",
|
||||
"previous_page": "Попередня сторінка"
|
||||
"search": "Фільтрувати…",
|
||||
"developers": "Розробники",
|
||||
"genres": "Жанри",
|
||||
"tags": "Теги",
|
||||
"publishers": "Видавці",
|
||||
"download_sources": "Джерела",
|
||||
"result_count": "{{resultCount}} результатів",
|
||||
"filter_count": "{{filterCount}} доступно",
|
||||
"clear_filters": "Очистити {{filterCount}} вибрані"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Відкрити варіанти завантаження",
|
||||
@@ -86,18 +102,31 @@
|
||||
"download_now": "Завантажити зараз",
|
||||
"calculating_eta": "Обчислення залишкового часу…",
|
||||
"create_shortcut": "Створити ярлик на робочому столі",
|
||||
"create_shortcut_success": "Ярлик успішно створено",
|
||||
"create_shortcut_error": "Виникла помилка під час створення ярлику",
|
||||
"nsfw_content_title": "Ця гра містить неприйнятний контент",
|
||||
"nsfw_content_description": "{{title}} містить вміст, який може бути непридатним для всіх вікових груп. Ви впевнені, що хочете продовжити?",
|
||||
"allow_nsfw_content": "Продовжити",
|
||||
"refuse_nsfw_content": "Вернутись назад",
|
||||
"stats": "Статистика",
|
||||
"clear": "Очистити",
|
||||
"danger_zone_section_description": "Видалити цю гру з вашої бібліотеки або файли скачані Hydra",
|
||||
"danger_zone_section_title": "Небезпечна зона",
|
||||
"player_count": "Грають зараз",
|
||||
"download": "Завантажити",
|
||||
"download_count": "Завантажень",
|
||||
"download_in_progress": "Триває завантаження.",
|
||||
"download_options": "Варіантів завантаження",
|
||||
"download_path": "Тека для завантажень",
|
||||
"download_paused": "Завантаження призупинено",
|
||||
"download_settings": "Налаштування завантаження",
|
||||
"download_error": "Цей варіант завантаження не доступний",
|
||||
"downloader": "Завантажувач",
|
||||
"downloads_secion_title": "Завантаження",
|
||||
"downloads_section_title": "Завантаження",
|
||||
"downloads_section_description": "Перевірити наявність оновлень або інших версій гри",
|
||||
"executable_section_description": "Шлях до файлу, який буде запущений при натисканні на кнопку \"Play\"",
|
||||
"executable_section_title": "Файл",
|
||||
"executable_path_in_use": "Виконуваний файл наразі використовується \"{{game}}\"",
|
||||
"last_downloaded_option": "Останній варіант завантаження",
|
||||
"next_screenshot": "Наступний скрішнот",
|
||||
"no_executable_selected": "Файл не вибрано",
|
||||
@@ -112,7 +141,64 @@
|
||||
"remove_from_library_description": "{{game}} буде видалено з вашої бібліотеки",
|
||||
"remove_from_library_title": "Ви впевнені?",
|
||||
"screenshot": "Скріншот",
|
||||
"select_executable": "Обрати"
|
||||
"select_executable": "Обрати",
|
||||
"warning": "Попередження:",
|
||||
"hydra_needs_to_remain_open": "Hydra повинна залишатись відкритою до завершення завантаження. Якщо Hydra закриється до завершення, ви втратите прогрес.",
|
||||
"achievements": "Досягнення",
|
||||
"achievements_count": "Досягнень {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Хмарне Збереження",
|
||||
"cloud_save_description": "Збережіть свій ігровий процесс у хмарі та продовжіть грати з любого пристрою",
|
||||
"backups": "Резервні копії",
|
||||
"install_backup": "Встановити",
|
||||
"delete_backup": "Видалити",
|
||||
"create_backup": "Нова резервна копія",
|
||||
"last_backup_date": "Остання резервна копія {{date}}",
|
||||
"no_backup_preview": "Не було знайдено жодних збережень для цієї гри.",
|
||||
"restoring_backup": "Відновлення резервної копії ({{progress}} виконано)…",
|
||||
"uploading_backup": "Вивантаження резервної копії…",
|
||||
"no_backups": "Ви ще не створили жодних резеврних копій для цієї гри",
|
||||
"backup_uploaded": "Резервна копія вивантажена",
|
||||
"backup_deleted": "Резервна копія видалена",
|
||||
"backup_restored": "Резервна копія відновлена",
|
||||
"see_all_achievements": "Переглянути всі досягнення",
|
||||
"sign_in_to_see_achievements": "Увійдіть, що б переглянути усі досягнення",
|
||||
"mapping_method_automatic": "Автоматичний",
|
||||
"mapping_method_manual": "Ручний",
|
||||
"mapping_method_label": "Спосіб визначення файлів для резервного копіювання",
|
||||
"files_automatically_mapped": "Hydra автоматично вибере файли для резервного копіювання",
|
||||
"no_backups_created": "Для цієї гри не було створено резервних копій",
|
||||
"manage_files": "Керувати файлами",
|
||||
"loading_save_preview": "Виконується пошук збережень гри...",
|
||||
"wine_prefix": "Префікс Wine",
|
||||
"wine_prefix_description": "Префікс Wine використовувався для запуску цієї гри",
|
||||
"launch_options": "Параметри загрузки",
|
||||
"launch_options_description": "Досвідчені користувачі можуть ввести власні модифікації до параметрів запуску (експериментальна функція).",
|
||||
"launch_options_placeholder": "Параметри не вказано",
|
||||
"no_download_option_info": "Немає інформації",
|
||||
"backup_deletion_failed": "Виникла помилка при видалені резервної копії",
|
||||
"max_number_of_artifacts_reached": "Максимальну кількість резервних копій для цієї гри досягнуто",
|
||||
"achievements_not_sync": "Дізнайтеся, як синхронізувати свої досягнення",
|
||||
"manage_files_description": "Керуйте файлами, які буде збережено та відновлено",
|
||||
"select_folder": "Обрати папку",
|
||||
"backup_from": "Резервна копія від {{date}}",
|
||||
"automatic_backup_from": "Автоматична резервна копія від {{date}}",
|
||||
"enable_automatic_cloud_sync": "Увімкнути автоматичну синхронізацію з хмарою",
|
||||
"custom_backup_location_set": "Встановлено користувацьке місце збереження резервної копії",
|
||||
"no_directory_selected": "Папку не було вибрано",
|
||||
"no_write_permission": "Неможливо завантажити у дану папку. Натисніть сюда щоб дізнатись більше.",
|
||||
"reset_achievements": "Скинути досягнення",
|
||||
"reset_achievements_description": "Це скине всі досягнення для {{game}}",
|
||||
"reset_achievements_title": "Ви впевнені?",
|
||||
"reset_achievements_success": "Досягнення успішно скинуто",
|
||||
"reset_achievements_error": "Не вдалося скинути досягнення",
|
||||
"download_error_gofile_quota_exceeded": "Ви перевищили місячну квоту Gofile. Будь ласка, дочекайтесь, поки квота скинеться.",
|
||||
"download_error_real_debrid_account_not_authorized": "Ваш обліковий запис Real-Debrid не авторизований для нових завантажень. Будь ласка, перевірте налаштування облікового запису та спробуйте ще раз.",
|
||||
"download_error_not_cached_on_real_debrid": "Це завантаження недоступне на Real-Debrid, і перевірка статусу завантаження з Real-Debrid наразі недоступна.",
|
||||
"download_error_not_cached_on_torbox": "Це завантаження недоступне на TorBox, і перевірка статусу завантаження з TorBox наразі недоступна.",
|
||||
"download_error_not_cached_on_hydra": "Це завантаження недоступне через Nimbus.",
|
||||
"game_removed_from_favorites": "Гра видалена з улюбленних",
|
||||
"game_added_to_favorites": "Гра була добавлена у улюблені",
|
||||
"automatically_extract_downloaded_files": "Автоматично розархівувати завантаженні файли"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активувати Hydra",
|
||||
@@ -144,7 +230,14 @@
|
||||
"no_downloads_title": "Тут так пусто...",
|
||||
"queued": "В черзі",
|
||||
"queued_downloads": "Завантаження в черзі",
|
||||
"removed": "Не завантажено"
|
||||
"removed": "Не завантажено",
|
||||
"checking_files": "Перевірка файлів…",
|
||||
"seeding": "Сідінг",
|
||||
"stop_seeding": "Зупинити сідінг",
|
||||
"resume_seeding": "Продовжити сідінг",
|
||||
"options": "Налаштування",
|
||||
"extract": "Розархівувати файли",
|
||||
"extracting": "Розархівовування файлів…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Тека завантажень",
|
||||
@@ -193,21 +286,101 @@
|
||||
"removed_download_source": "Джерело завантажень було видалено",
|
||||
"save_changes": "Зберегти зміни",
|
||||
"sync_download_sources": "Синхронізувати джерела",
|
||||
"validate_download_source": "Перевірити"
|
||||
"validate_download_source": "Перевірити",
|
||||
"public": "Публічний",
|
||||
"private": "Приватний",
|
||||
"friends_only": "Тільки для друзів",
|
||||
"privacy": "Приватність",
|
||||
"profile_visibility": "Видимість профілю",
|
||||
"profile_visibility_description": "Оберіть хто може бачити ваш профіль та бібліотеку",
|
||||
"required_field": "Це поле обов'язкове",
|
||||
"source_already_exists": "Це джерело уже додано",
|
||||
"must_be_valid_url": "Джерело має бути дійсною URL-адресою",
|
||||
"blocked_users": "Заблоковані користувачі",
|
||||
"user_unblocked": "Користувача було розблоковано",
|
||||
"enable_achievement_notifications": "Коли отримано нове досягнення",
|
||||
"launch_minimized": "Запустити Hydra у згорнутому вигляді",
|
||||
"disable_nsfw_alert": "Вимкнути сповіщення про NSFW",
|
||||
"seed_after_download_complete": "Cідувати по завершенню завантажень",
|
||||
"show_hidden_achievement_description": "Показувати опис прихованих досягнень до їх розблокування",
|
||||
"account": "Акаунт",
|
||||
"no_users_blocked": "У вас немає заблокованих користувачів",
|
||||
"subscription_active_until": "Ваша підписка на Hydra Cloud активна до {{date}}",
|
||||
"manage_subscription": "Керування підпискою",
|
||||
"update_email": "Змінити електронну пошту",
|
||||
"update_password": "Зміна паролю",
|
||||
"current_email": "Поточна електронна пошта",
|
||||
"no_email_account": "Ви ще не вказали електронну пошту",
|
||||
"account_data_updated_successfully": "Дані облікового запису успішно оновлено",
|
||||
"renew_subscription": "Поновити Hydra Cloud",
|
||||
"subscription_expired_at": "Ваша підписка закінчилася {{date}}",
|
||||
"no_subscription": "Насолоджуйтесь Hydra найкращим можливим чином",
|
||||
"become_subscriber": "Придбати підписку Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Автоматичне поновлення виключено",
|
||||
"subscription_renews_on": "Ваша підписка поновиться {{date}}",
|
||||
"bill_sent_until": "Ваш наступний рахунок буде надіслано до цього дня",
|
||||
"no_themes": "Схоже, у вас ще немає тем, але не хвилюйтеся, натисніть тут, щоб створити свій перший шедевр.",
|
||||
"editor_tab_code": "Код",
|
||||
"editor_tab_info": "Інформація",
|
||||
"editor_tab_save": "Зберегти",
|
||||
"web_store": "Інтернет-магазин",
|
||||
"clear_themes": "Очистити",
|
||||
"create_theme": "Створити",
|
||||
"create_theme_modal_title": "Створити користувацьку тему",
|
||||
"create_theme_modal_description": "Створіть нову тему для налаштування зовнішнього вигляду Hydra",
|
||||
"theme_name": "Назва",
|
||||
"insert_theme_name": "Введіть назву теми",
|
||||
"set_theme": "Включити тему",
|
||||
"unset_theme": "Виключити тему",
|
||||
"delete_theme": "Видалити тему",
|
||||
"edit_theme": "Редагувати тему",
|
||||
"delete_all_themes": "Видалити усі теми",
|
||||
"delete_all_themes_description": "Це видалить усі ваші користувацькі теми",
|
||||
"delete_theme_description": "Це видалить тему {{theme}}",
|
||||
"cancel": "Відмінити",
|
||||
"appearance": "Вигляд",
|
||||
"enable_torbox": "Включити TorBox",
|
||||
"torbox_description": "TorBox — це ваш преміум-сервіс для сідінгу, що конкурує навіть з найкращими серверами на ринку.",
|
||||
"torbox_account_linked": "TorBox акаунт прив'язано",
|
||||
"create_real_debrid_account": "Натисніть тут, якщо у вас ще немає облікового запису Real-Debrid",
|
||||
"create_torbox_account": "Натисніть тут, якщо у вас ще немає облікового запису TorBox",
|
||||
"real_debrid_account_linked": "Real-Debrid акаунт прив'язано",
|
||||
"name_min_length": "Назва теми повинна містити не менше 3 символів",
|
||||
"import_theme": "Імпортувати тему",
|
||||
"import_theme_description": "Ви імпортуєте {{theme}} з магазину тем",
|
||||
"error_importing_theme": "Помилка при імпорті теми",
|
||||
"theme_imported": "Тема успішно імпортована",
|
||||
"enable_friend_request_notifications": "При отриманні запиту на дружбу",
|
||||
"enable_auto_install": "Автоматично завантажувати оновлення",
|
||||
"common_redist": "Загальні розповсюджувані компоненти",
|
||||
"common_redist_description": "Загальні розповсюджувані компоненти необхідні для запуску деяких ігор. Рекомендується їх встановити, щоб уникнути проблем.",
|
||||
"install_common_redist": "Встановити",
|
||||
"installing_common_redist": "Встановлюється…",
|
||||
"show_download_speed_in_megabytes": "Показувати швидкість завантаження в мегабайтах на секунду"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Завантаження завершено",
|
||||
"game_ready_to_install": "{{title}} готова до встановлення",
|
||||
"repack_list_updated": "Список репаків оновлено",
|
||||
"repack_count_one": "{{count}} репак додано",
|
||||
"repack_count_other": "{{count}} репаків додано"
|
||||
"repack_count_other": "{{count}} репаків додано",
|
||||
"new_update_available": "Версія {{version}} доступна",
|
||||
"restart_to_install_update": "Перезавантажте Hydra, щоб встановити оновлення",
|
||||
"notification_achievement_unlocked_title": "Розблоковано досягнення для {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} та інші {{count}} було розблоковано",
|
||||
"new_friend_request_description": "Ви отримали новий запит на дружбу",
|
||||
"new_friend_request_title": "Новий запит на дружбу",
|
||||
"extraction_complete": "Витягування завершено",
|
||||
"game_extracted": "{{title}} успішно витягнуто"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Відкрити Hydra",
|
||||
"quit": "Вийти"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "Немає доступних завантажень"
|
||||
"no_downloads": "Немає доступних завантажень",
|
||||
"available_one": "Доступний",
|
||||
"available_other": "Доступні"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Програми не встановлені",
|
||||
@@ -240,6 +413,94 @@
|
||||
"sign_out_modal_title": "Ви впевнені?",
|
||||
"successfully_signed_out": "Успішний вихід з акаунту",
|
||||
"total_play_time": "Всього зіграно",
|
||||
"try_again": "Будь ласка, попробуйте ще раз"
|
||||
"try_again": "Будь ласка, попробуйте ще раз",
|
||||
"add_friends": "Добавити друзів",
|
||||
"add": "Добавити",
|
||||
"friend_code": "Код друга",
|
||||
"see_profile": "Переглянути профіль",
|
||||
"sending": "Надсилання",
|
||||
"friend_request_sent": "Запит на дружбу було надіслано",
|
||||
"friends": "Друзі",
|
||||
"friends_list": "Список друзів",
|
||||
"user_not_found": "Користувача не найдено",
|
||||
"block_user": "Заблокувати користувача",
|
||||
"add_friend": "Добавити друга",
|
||||
"request_sent": "Запит надіслано",
|
||||
"request_received": "Запит отримано",
|
||||
"accept_request": "Прийняти запит",
|
||||
"ignore_request": "Ігнорувати запит",
|
||||
"cancel_request": "Скасувати запит",
|
||||
"undo_friendship": "Видалити з друзів",
|
||||
"request_accepted": "Запит прийнято",
|
||||
"user_blocked_successfully": "Користувач успішно заблокований",
|
||||
"user_block_modal_text": "Це заблокує {{displayName}}",
|
||||
"blocked_users": "Заблоковані користувачі",
|
||||
"unblock": "Розблокувати",
|
||||
"no_friends_added": "У вас немає доданих друзів",
|
||||
"pending": "Очікування",
|
||||
"no_pending_invites": "У вас немає запитів, що очікують на підтвердження",
|
||||
"no_blocked_users": "У вас немає заблокованих користувачів",
|
||||
"friend_code_copied": "Код друга скопійовано",
|
||||
"undo_friendship_modal_text": "Це видалить з друзів {{displayName}}",
|
||||
"privacy_hint": "Щоб налаштувати, хто може це бачити, перейдіть до <0>Settings</0>",
|
||||
"locked_profile": "Цей профіль приватний",
|
||||
"image_process_failure": "Помилка при обробці зображення",
|
||||
"required_field": "Це поле обов'язкове",
|
||||
"displayname_min_length": "Ім'я користувача повинно містити не менше 3 символів",
|
||||
"displayname_max_length": "Ім'я користувача повинно містити не більше 50 символів",
|
||||
"report_profile": "Повідомити про цей профіль",
|
||||
"report_reason": "Чому ви повідомляєте про цей профіль?",
|
||||
"report_description": "Додаткова інформація",
|
||||
"report_description_placeholder": "Додаткова інформація",
|
||||
"report": "Повідомити",
|
||||
"report_reason_hate": "Мова ненависті",
|
||||
"report_reason_sexual_content": "Сексуальний контент",
|
||||
"report_reason_violence": "Насильство",
|
||||
"report_reason_spam": "Спам",
|
||||
"report_reason_other": "Інше",
|
||||
"profile_reported": "Профіль повідомлено",
|
||||
"your_friend_code": "Ваш код друга:",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Завантажити банер...",
|
||||
"background_image_updated": "Фонове зображення оновлено",
|
||||
"stats": "Статистика",
|
||||
"achievements": "Досягнення",
|
||||
"games": "Ігри",
|
||||
"top_percentile": "Топ {{percentile}}%",
|
||||
"ranking_updated_weekly": "Рейтинг оновлюється щотижня",
|
||||
"playing": "Грає у {{game}}",
|
||||
"achievements_unlocked": "Досягнень розблоковано",
|
||||
"earned_points": "Отримано балів",
|
||||
"show_achievements_on_profile": "Покажіть свої досягнення у своєму профілі",
|
||||
"show_points_on_profile": "Покажіть ваші отриманні бали у своєму профілі"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Досягнення розблоковано",
|
||||
"user_achievements": "Досягнення користувача {{displayName}}",
|
||||
"your_achievements": "Ваші досягнення",
|
||||
"unlocked_at": "Розблоковано: {{date}}",
|
||||
"subscription_needed": "Для перегляду цього контенту потрібна підписка на Hydra Cloud",
|
||||
"new_achievements_unlocked": "Розблоковано {{achievementCount}} нових досягнень з {{gameCount}} ігор",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} досягнень",
|
||||
"achievements_unlocked_for_game": "Розблоковано {{achievementCount}} нових досягнень у грі {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Це приховане досягнення",
|
||||
"achievement_earn_points": " Отримайте {{points}} балів за це досягнення",
|
||||
"earned_points": "Отримано балів:",
|
||||
"available_points": "Доступно балів:",
|
||||
"how_to_earn_achievements_points": "Як заробляти бали за досягнення?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Підписка Hydra Cloud",
|
||||
"subscribe_now": "Підписатися зараз",
|
||||
"cloud_saving": "Збереження в хмарі",
|
||||
"cloud_achievements": "Зберігайте свої досягнення в хмарі",
|
||||
"animated_profile_picture": "Анімовані аватари",
|
||||
"premium_support": "Преміум-підтримка",
|
||||
"show_and_compare_achievements": "Показуйте та порівнюйте свої досягнення з іншими користувачами",
|
||||
"animated_profile_banner": "Анімований банер профілю",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Ви щойно виявили функцію Hydra Cloud!",
|
||||
"learn_more": "Дізнатися більше",
|
||||
"debrid_description": "Завантажуйте до 4 разів швидше з Nimbus"
|
||||
}
|
||||
}
|
||||
|
||||
476
src/locales/uz/translation.json
Normal file
476
src/locales/uz/translation.json
Normal file
@@ -0,0 +1,476 @@
|
||||
{
|
||||
"language_name": "Uzbek",
|
||||
"app": {
|
||||
"successfully_signed_in": "Kirish muvaffaqiyatli amalga oshirildi"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Tavsiya etilgan",
|
||||
"surprise_me": "Hayratda qoldir",
|
||||
"no_results": "Natijalar topilmadi",
|
||||
"hot": "Eng mashhur",
|
||||
"start_typing": "Yozishni boshlayapman ...",
|
||||
"weekly": "📅 Haftaning eng yaxshi o'yinlari",
|
||||
"achievements": "🏆 Yutuqlar bilan o'yinlar"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Yuklab olishlar",
|
||||
"settings": "Sozlamalar",
|
||||
"my_library": "Mening kutubxonam",
|
||||
"downloading_metadata": "{{title}} (Metamaʼlumotlar yuklanmoqda…)",
|
||||
"paused": "{{title}} (To'xtatildi)",
|
||||
"downloading": "{{title}} ({{percentage}} - Yuklanmoqda…)",
|
||||
"filter": "Qidiruv",
|
||||
"home": "Asosiy",
|
||||
"queued": "{{title}} (Navbatda)",
|
||||
"game_has_no_executable": "Oʻyinni ishga tushirish fayli tanlanmagan",
|
||||
"sign_in": "Kirish",
|
||||
"friends": "Do'stlar",
|
||||
"need_help": "Yordam kerakmi?",
|
||||
"favorites": "Sevimlilar"
|
||||
},
|
||||
"header": {
|
||||
"search": "Qidirish",
|
||||
"home": "Asosiy",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Yuklab olishlar",
|
||||
"search_results": "Qidiruv natijalari",
|
||||
"settings": "Sozlamalar",
|
||||
"version_available_install": "{{version}} versiyasi mavjud. Oʻrnatish uchun shu yerni bosing.",
|
||||
"version_available_download": "{{version}} versiyasi mavjud. Yuklab olish uchun shu yerni bosing."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Faol yuklab olishlar yo'q",
|
||||
"downloading_metadata": "Metamaʼlumotlar yuklanmoqda {{title}}…",
|
||||
"downloading": "Yuklanmoqda {{title}}… ({{percentage}} yakunlandi) - Tugash vaqti {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Yuklanmoqda {{title}}… ({{percentage}} yakunlandi) - Qolgan vaqt hisoblanmoqda…",
|
||||
"checking_files": "Fayllar tekshirilmoqda {{title}}… ({{percentage}} yakunlandi)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "O'rnatish yakunlandi",
|
||||
"installation_complete_message": "Kutubxonalar muvaffaqiyatli o'rnatildi"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filter…",
|
||||
"developers": "Ishlab chiquvchilar",
|
||||
"genres": "Janrlar",
|
||||
"tags": "Teglar",
|
||||
"publishers": "Nashriyotlar",
|
||||
"download_sources": "Yuklab olish manbalari",
|
||||
"result_count": "{{resultCount}} natija",
|
||||
"filter_count": "{{filterCount}} mavjud",
|
||||
"clear_filters": "{{filterCount}} tanlangan filtrni tozalash"
|
||||
},
|
||||
"game_details": {
|
||||
"play_time": "O'ynalgan vaqt {{amount}}",
|
||||
"last_time_played": "Oxirgi ishga tushirilgan {{period}}",
|
||||
"not_played_yet": "Siz hali {{title}}ni o'ynamagansiz",
|
||||
"next_suggestion": "Keyingi taklif",
|
||||
"play": "O'ynash",
|
||||
"deleting": "O'rnatuvchi o'chirilmoqda…",
|
||||
"close": "Yopish",
|
||||
"playing_now": "Hozir o'ynalmoqda",
|
||||
"change": "O'zgartirish",
|
||||
"repacks_modal_description": "Yuklab olish uchun repakni tanlang",
|
||||
"select_folder_hint": "Standart yuklab olish jildini o'zgartirish uchun <0>Sozlamalar</0>ni oching",
|
||||
"download_now": "Hozir yuklab olish",
|
||||
"no_shop_details": "Tavsif olib bo'lmadi",
|
||||
"download_options": "Manbalar",
|
||||
"download_path": "Yuklab olish yo'li",
|
||||
"previous_screenshot": "Oldingi skrinshot",
|
||||
"next_screenshot": "Keyingi skrinshot",
|
||||
"screenshot": "Skrinshot {{number}}",
|
||||
"open_screenshot": "{{number}}-skrinshotni ochish",
|
||||
"download_settings": "Yuklab olish sozlamalari",
|
||||
"downloader": "Yuklovchi",
|
||||
"select_executable": "Tanlash",
|
||||
"no_executable_selected": "Fayl tanlanmagan",
|
||||
"open_folder": "Jildni ochish",
|
||||
"open_download_location": "Yuklab olish jildini ko'rish",
|
||||
"create_shortcut": "Ish stoliga yorliq yaratish",
|
||||
"clear": "Tozalash",
|
||||
"remove_files": "Fayllarni o'chirish",
|
||||
"remove_from_library_title": "Ishonchingiz komilmi?",
|
||||
"remove_from_library_description": "{{game}} kutubxonangizdan o'chiriladi.",
|
||||
"options": "Sozlamalar",
|
||||
"executable_section_title": "Fayl",
|
||||
"executable_section_description": "\"O'ynash\" tugmasi bosilganda ishga tushiriladigan fayl yo'li",
|
||||
"downloads_section_title": "Yuklab olishlar",
|
||||
"downloads_section_description": "Yangilanishlar yoki o'yinning boshqa versiyalarini tekshirish",
|
||||
"danger_zone_section_title": "Xavfli zona",
|
||||
"danger_zone_section_description": "Bu o'yinni kutubxonangizdan yoki Hydradan yuklab olingan fayllarni o'chirishingiz mumkin",
|
||||
"download_in_progress": "Yuklab olish davom etmoqda",
|
||||
"download_paused": "Yuklab olish to'xtatilgan",
|
||||
"last_downloaded_option": "Oxirgi yuklab olish varianti",
|
||||
"create_shortcut_success": "Yorliq yaratildi",
|
||||
"create_shortcut_error": "Yorliq yaratib bo'lmadi",
|
||||
"allow_nsfw_content": "Davom etish",
|
||||
"download": "Yuklab olish",
|
||||
"download_count": "Yuklab olishlar",
|
||||
"download_error": "Bu yuklab olish varianti mavjud emas",
|
||||
"executable_path_in_use": "Bajarish fayli allaqachon \"{{game}}\" tomonidan ishlatilmoqda",
|
||||
"nsfw_content_description": "{{title}} barcha yoshdagilar uchun mos bo'lmasligi mumkin bo'lgan tarkibni o'z ichiga oladi. \nDavom etishni xohlaysizmi?",
|
||||
"nsfw_content_title": "Bu o'yin nomunosib tarkibga ega",
|
||||
"refuse_nsfw_content": "Orqaga",
|
||||
"stats": "Statistika",
|
||||
"player_count": "Faol o'yinchilar",
|
||||
"warning": "Ogohlantirish:",
|
||||
"hydra_needs_to_remain_open": "Bu yuklab olish uchun Hydra tugaguncha ochiq qolishi kerak. Agar Hydra tugashidan oldin yopilsa, jarayonni yo'qotasiz.",
|
||||
"achievements": "Yutuqlar",
|
||||
"achievements_count": "Yutuqlar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Bulutli saqlash",
|
||||
"cloud_save_description": "O'yin jarayoningizni bulutda saqlang va istalgan qurilmada o'ynashni davom ettiring",
|
||||
"backups": "Zaxira nusxalar",
|
||||
"install_backup": "O'rnatish",
|
||||
"delete_backup": "O'chirish",
|
||||
"create_backup": "Yangi zaxira nusxa yaratish",
|
||||
"last_backup_date": "Oxirgi zaxira nusxa {{date}} dan",
|
||||
"no_backup_preview": "Bu sarlavha uchun saqlashlar topilmadi",
|
||||
"restoring_backup": "Zaxira nusxa tiklanmoqda ({{progress}} yakunlandi)…",
|
||||
"uploading_backup": "Zaxira nusxa yuklanmoqda…",
|
||||
"no_backups": "Siz hali bu o'yin uchun zaxira nusxa yaratmagansiz",
|
||||
"backup_uploaded": "Zaxira nusxa yuklandi",
|
||||
"backup_deleted": "Zaxira nusxa o'chirildi",
|
||||
"backup_restored": "Zaxira nusxa tiklandi",
|
||||
"see_all_achievements": "Barcha yutuqlarni ko'rish",
|
||||
"sign_in_to_see_achievements": "Yutuqlarni ko'rish uchun tizimga kiring",
|
||||
"mapping_method_automatic": "Avtomatik",
|
||||
"mapping_method_manual": "Qo'lda",
|
||||
"mapping_method_label": "Moslashtirish usuli",
|
||||
"files_automatically_mapped": "Fayllar avtomatik moslashtirildi",
|
||||
"no_backups_created": "Bu o'yin uchun zaxira nusxalar yaratilmagan",
|
||||
"manage_files": "Fayllarni boshqarish",
|
||||
"loading_save_preview": "Saqlashlar qidirilmoqda…",
|
||||
"wine_prefix": "Wine prefiksi",
|
||||
"wine_prefix_description": "Bu o'yinni ishga tushirish uchun ishlatiladigan Wine prefiksi",
|
||||
"launch_options": "Ishga tushirish parametrlari",
|
||||
"launch_options_description": "Tajribali foydalanuvchilar ishga tushirish parametrlarini o'zgartirishi mumkin",
|
||||
"launch_options_placeholder": "Parametr ko'rsatilmagan",
|
||||
"no_download_option_info": "Ma'lumot mavjud emas",
|
||||
"backup_deletion_failed": "Zaxira nusxani o'chirib bo'lmadi",
|
||||
"max_number_of_artifacts_reached": "Bu o'yin uchun maksimal zaxira nusxalar soniga yetildi",
|
||||
"achievements_not_sync": "Yutuqlaringiz sinxronlanmagan",
|
||||
"manage_files_description": "Saqlanishi va tiklanishi kerak bo'lgan fayllarni boshqaring",
|
||||
"select_folder": "Jildni tanlash",
|
||||
"backup_from": "{{date}} dan zaxira nusxa",
|
||||
"automatic_backup_from": "{{date}} dan avtomatik zaxira nusxa",
|
||||
"enable_automatic_cloud_sync": "Avtomatik bulutli sinxronlashni yoqish",
|
||||
"custom_backup_location_set": "Maxsus zaxira nusxa joylashuvi o'rnatildi",
|
||||
"no_directory_selected": "Katalog tanlanmagan",
|
||||
"no_write_permission": "Bu katalogga yuklab bo'lmaydi. Ko'proq ma'lumot olish uchun bu yerga bosing.",
|
||||
"reset_achievements": "Yutuqlarni tiklash",
|
||||
"reset_achievements_description": "Bu {{game}} uchun barcha yutuqlarni tiklaydi",
|
||||
"reset_achievements_title": "Ishonchingiz komilmi?",
|
||||
"reset_achievements_success": "Yutuqlar muvaffaqiyatli tiklandi",
|
||||
"reset_achievements_error": "Yutuqlarni tiklab bo'lmadi",
|
||||
"download_error_gofile_quota_exceeded": "Siz Gofile oylik kvotangizni oshirib yubordingiz. Iltimos, kvota tiklanguncha kuting.",
|
||||
"download_error_real_debrid_account_not_authorized": "Sizning Real-Debrid hisobingiz yangi yuklab olishlar uchun avtorizatsiya qilinmagan. Iltimos, hisob sozlamalarini tekshiring va qaytadan urinib ko'ring.",
|
||||
"download_error_not_cached_on_real_debrid": "Bu yuklab olish Real-Debrid'da mavjud emas, va Real-Debrid'dan yuklab olish holatini olish hozircha mavjud emas.",
|
||||
"download_error_not_cached_on_torbox": "Bu yuklab olish TorBox'da mavjud emas, va TorBox'dan yuklab olish holatini olish hozircha mumkin emas.",
|
||||
"game_added_to_favorites": "O'yin sevimlilarga qo'shildi",
|
||||
"game_removed_from_favorites": "O'yin sevimlilardan olib tashlandi",
|
||||
"automatically_extract_downloaded_files": "Yuklab olingan fayllarni avtomatik chiqarish"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'ni faollashtirish",
|
||||
"installation_id": "O'rnatish ID'si:",
|
||||
"enter_activation_code": "Faollashtirish kodini kiriting",
|
||||
"message": "Agar qayerdan so'rash kerakligini bilmasangiz, u sizda bo'lmasligi kerak.",
|
||||
"activate": "Faollashtirish",
|
||||
"loading": "Yuklanmoqda…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Davom ettirish",
|
||||
"pause": "To'xtatib turish",
|
||||
"eta": "Tugash vaqti {{eta}}",
|
||||
"paused": "To'xtatilgan",
|
||||
"verifying": "Tekshirilmoqda…",
|
||||
"completed": "Yakunlandi",
|
||||
"removed": "Yuklab olinmagan",
|
||||
"cancel": "Bekor qilish",
|
||||
"filter": "Yuklab olingan o'yinlarni qidirish",
|
||||
"remove": "O'chirish",
|
||||
"downloading_metadata": "Metamaʼlumotlar yuklanmoqda…",
|
||||
"deleting": "O'rnatuvchi o'chirilmoqda…",
|
||||
"delete": "O'rnatuvchini o'chirish",
|
||||
"delete_modal_title": "Ishonchingiz komilmi?",
|
||||
"delete_modal_description": "Bu kompyuteringizdan barcha o'rnatuvchilarni o'chiradi",
|
||||
"install": "O'rnatish",
|
||||
"download_in_progress": "Jarayonda",
|
||||
"queued_downloads": "Navbatdagi yuklab olishlar",
|
||||
"downloads_completed": "Yakunlangan",
|
||||
"queued": "Navbatda",
|
||||
"no_downloads_title": "Bu yer juda bo'sh...",
|
||||
"no_downloads_description": "Siz hali Hydra orqali hech narsa yuklab olmadingiz, lekin boshlash uchun hech qachon kech emas.",
|
||||
"checking_files": "Fayllar tekshirilmoqda…",
|
||||
"seeding": "Ulashish",
|
||||
"stop_seeding": "Ulashishni to'xtatish",
|
||||
"resume_seeding": "Ulashishni davom ettirish",
|
||||
"options": "Boshqarish",
|
||||
"extract": "Fayllarni chiqarish",
|
||||
"extracting": "Fayllar chiqarilmoqda…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Yuklab olish yo'li",
|
||||
"change": "O'zgartirish",
|
||||
"notifications": "Bildirishnomalar",
|
||||
"enable_download_notifications": "Yuklab olish tugaganda",
|
||||
"enable_achievement_notifications": "Yutuq ochilganda",
|
||||
"enable_repack_list_notifications": "Yangi repak qo'shilganda",
|
||||
"real_debrid_api_token_label": "Real-Debrid API-tokeni",
|
||||
"quit_app_instead_hiding": "Ilovani trayga yig'ish o'rniga yopish",
|
||||
"launch_with_system": "Hydra'ni tizim bilan birga ishga tushirish",
|
||||
"launch_minimized": "Hydra'ni yig'ilgan holda ishga tushirish",
|
||||
"disable_nsfw_alert": "Noqulay kontent haqida ogohlantirishni o'chirish",
|
||||
"general": "Asosiy",
|
||||
"behavior": "Xatti-harakat",
|
||||
"download_sources": "Yuklab olish manbalari",
|
||||
"language": "Til",
|
||||
"api_token": "API kalit",
|
||||
"enable_real_debrid": "Real-Debrid'ni yoqish",
|
||||
"real_debrid_description": "Real-Debrid - bu cheksiz yuklab oluvchi bo'lib, internetda joylashtirilgan fayllarni tezda yuklab olishga yoki ularni xususiy tarmoq orqali pleerga zudlik bilan o'tkazishga imkon beradi, bu esa har qanday blokirovkalarni chetlab o'tishga imkon beradi.",
|
||||
"debrid_invalid_token": "Noto'g'ri API kalit",
|
||||
"debrid_api_token_hint": "API kalitni <0>bu yerda</0> olish mumkin",
|
||||
"real_debrid_free_account_error": "\"{{username}}\" hisobi - obunaga ega emas. Iltimos, Real-Debrid obunasini rasmiylashtiring",
|
||||
"debrid_linked_message": "\"{{username}}\" hisobi bog'langan",
|
||||
"save_changes": "O'zgarishlarni saqlash",
|
||||
"changes_saved": "O'zgarishlar muvaffaqiyatli saqlandi",
|
||||
"download_sources_description": "Hydra yuklab olish havolalarini ushbu manbalardan oladi. URL yuklab olish uchun havolalar bilan .json-fayliga to'g'ridan-to'g'ri havolani o'z ichiga olishi kerak.",
|
||||
"validate_download_source": "Tekshirish",
|
||||
"remove_download_source": "O'chirish",
|
||||
"add_download_source": "Manba qo'shish",
|
||||
"download_count_zero": "Ro'yxatda yuklab olishlar yo'q",
|
||||
"download_count_one": "Ro'yxatda {{countFormatted}} ta yuklab olish",
|
||||
"download_count_other": "Ro'yxatda {{countFormatted}} ta yuklab olish",
|
||||
"download_source_url": "Manba havolasi",
|
||||
"add_download_source_description": ".json-fayliga havolani joylang",
|
||||
"download_source_up_to_date": "Yangilangan",
|
||||
"download_source_errored": "Xato",
|
||||
"sync_download_sources": "Manbalarni yangilash",
|
||||
"removed_download_source": "Manba o'chirildi",
|
||||
"cancel_button_confirmation_delete_all_sources": "Yo'q",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ha, barchasini o'chirish",
|
||||
"description_confirmation_delete_all_sources": "Siz barcha manbalarni o'chirasiz",
|
||||
"title_confirmation_delete_all_sources": "Barcha manbalarni o'chirish",
|
||||
"removed_download_sources": "Manbalar o'chirildi",
|
||||
"button_delete_all_sources": "Barcha manbalarni o'chirish",
|
||||
"added_download_source": "Manba qo'shildi",
|
||||
"download_sources_synced": "Barcha manbalar yangilandi",
|
||||
"insert_valid_json_url": "Amaldagi JSON-fayl URL'ini kiriting",
|
||||
"found_download_option_zero": "Yuklab olish variantlari topilmadi",
|
||||
"found_download_option_one": "{{countFormatted}} yuklab olish varianti topildi",
|
||||
"found_download_option_other": "{{countFormatted}} yuklab olish varianti topildi",
|
||||
"import": "Import qilish",
|
||||
"blocked_users": "Bloklangan foydalanuvchilar",
|
||||
"friends_only": "Faqat do'stlar uchun",
|
||||
"must_be_valid_url": "Manbada to'g'ri URL bo'lishi kerak",
|
||||
"privacy": "Maxfiylik",
|
||||
"private": "Shaxsiy",
|
||||
"profile_visibility": "Profil ko'rinuvchanligi",
|
||||
"profile_visibility_description": "Kim sizning profilingiz va kutubxonangizni ko'ra olishini tanlang",
|
||||
"public": "Ommaviy",
|
||||
"required_field": "Bu maydon to'ldirilishi shart",
|
||||
"source_already_exists": "Bu manba allaqachon qo'shilgan",
|
||||
"user_unblocked": "Foydalanuvchi blokdan chiqarildi",
|
||||
"seed_after_download_complete": "Yuklab olish tugagandan so'ng ulashish",
|
||||
"show_hidden_achievement_description": "Yashirin yutuqlarning tavsifini ularni olishdan oldin ko'rsatish",
|
||||
"account": "Hisob",
|
||||
"no_users_blocked": "Sizda bloklangan foydalanuvchilar yo'q",
|
||||
"subscription_active_until": "Hydra Cloud obunangiz {{date}} ga qadar faol",
|
||||
"manage_subscription": "Obunani boshqarish",
|
||||
"update_email": "Elektron pochtani yangilash",
|
||||
"update_password": "Parolni yangilash",
|
||||
"current_email": "Joriy elektron pochta:",
|
||||
"no_email_account": "Siz hali elektron pochta o'rnatmagansiz",
|
||||
"account_data_updated_successfully": "Hisob ma'lumotlari muvaffaqiyatli yangilandi",
|
||||
"renew_subscription": "Hydra Cloud obunasini yangilash",
|
||||
"subscription_expired_at": "Obunangiz muddati {{date}} da tugagan",
|
||||
"no_subscription": "Hydra'dan maksimal darajada bahramand bo'ling",
|
||||
"become_subscriber": "Hydra Cloud egasiga aylaning",
|
||||
"subscription_renew_cancelled": "Avtomatik yangilash o'chirilgan",
|
||||
"subscription_renews_on": "Obunangiz {{date}} da yangilanadi",
|
||||
"bill_sent_until": "Keyingi hisobingiz shu kungacha yuboriladi",
|
||||
"no_themes": "Sizda hali mavzular yo'qqa o'xshaydi, lekin tashvishlanmang, birinchi shoh asaringizni yaratish uchun shu yerni bosing",
|
||||
"editor_tab_code": "Kod",
|
||||
"editor_tab_info": "Ma'lumot",
|
||||
"editor_tab_save": "Saqlash",
|
||||
"web_store": "Veb-do'kon",
|
||||
"clear_themes": "Tozalash",
|
||||
"create_theme": "Yaratish",
|
||||
"create_theme_modal_title": "Maxsus mavzu yaratish",
|
||||
"create_theme_modal_description": "Hydra ko'rinishini sozlash uchun yangi mavzu yaratish",
|
||||
"theme_name": "Nomi",
|
||||
"insert_theme_name": "Mavzu nomini kiriting",
|
||||
"set_theme": "Mavzuni o'rnatish",
|
||||
"unset_theme": "Mavzuni olib tashlash",
|
||||
"delete_theme": "Mavzuni o'chirish",
|
||||
"edit_theme": "Mavzuni tahrirlash",
|
||||
"delete_all_themes": "Barcha mavzularni o'chirish",
|
||||
"delete_all_themes_description": "Bu barcha maxsus mavzularingizni o'chiradi",
|
||||
"delete_theme_description": "Bu {{theme}} mavzusini o'chirishga olib keladi",
|
||||
"cancel": "Bekor qilish",
|
||||
"appearance": "Tashqi ko'rinish",
|
||||
"enable_torbox": "TorBox'ni yoqish",
|
||||
"torbox_description": "TorBox - bu bozordagi eng yaxshi serverlar bilan ham raqobatlashadigan premium xizmatingiz.",
|
||||
"torbox_account_linked": "TorBox hisobi bog'langan",
|
||||
"real_debrid_account_linked": "Real-Debrid hisobi bog'langan",
|
||||
"create_real_debrid_account": "Agar sizda hali Real-Debrid hisobi bo'lmasa, shu yerni bosing",
|
||||
"create_torbox_account": "Agar sizda hali TorBox hisobi bo'lmasa, shu yerni bosing",
|
||||
"name_min_length": "Mavzu nomi kamida 3 ta belgi bo'lishi kerak",
|
||||
"import_theme": "Mavzuni import qilish",
|
||||
"import_theme_description": "Siz {{theme}} mavzusini mavzular do'konidan import qilmoqdasiz",
|
||||
"error_importing_theme": "Mavzuni import qilishda xato",
|
||||
"theme_imported": "Mavzu muvaffaqiyatli import qilindi",
|
||||
"enable_friend_request_notifications": "Do'stlar so'rovi olinganda",
|
||||
"enable_auto_install": "Yangilanishlarni avtomatik yuklab olish",
|
||||
"common_redist": "Kutubxonalar",
|
||||
"common_redist_description": "Ba'zi o'yinlarni ishga tushirish uchun kutubxonalar talab qilinadi. Muammolarning oldini olish uchun ularni o'rnatish tavsiya etiladi.",
|
||||
"install_common_redist": "O'rnatish",
|
||||
"installing_common_redist": "O'rnatilmoqda…",
|
||||
"show_download_speed_in_megabytes": "Yuklab olish tezligini sekundiga megabaytlarda ko'rsatish"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Yuklab olish yakunlandi",
|
||||
"game_ready_to_install": "{{title}} o'rnatishga tayyor",
|
||||
"repack_list_updated": "Repaklar ro'yxati yangilandi",
|
||||
"repack_count_one": "{{count}} repak qo'shildi",
|
||||
"repack_count_other": "{{count}} repak qo'shildi",
|
||||
"new_update_available": "Yangi {{version}} versiyasi mavjud",
|
||||
"restart_to_install_update": "Yangilanishni o'rnatish uchun Hydra'ni qayta ishga tushiring",
|
||||
"notification_achievement_unlocked_title": "{{game}} uchun yutuq ochildi",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} va boshqa {{count}} ta yutuq ochildi",
|
||||
"new_friend_request_title": "Yangi do'stlik so'rovi",
|
||||
"new_friend_request_description": "Siz yangi do'stlik so'rovini oldingiz",
|
||||
"extraction_complete": "Arxivdan chiqarish yakunlandi",
|
||||
"game_extracted": "{{title}} muvaffaqiyatli arxivdan chiqarildi"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra'ni ochish",
|
||||
"quit": "Chiqish"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Mavjud",
|
||||
"available_other": "Mavjud",
|
||||
"no_downloads": "Mavjud manbalar yo'q"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Dasturlar o'rnatilmagan",
|
||||
"description": "Wine yoki Lutris topilmadi",
|
||||
"instructions": "O'yin to'g'ri ishlashi uchun Linux distributivingizga ulardan birini o'rnatishning to'g'ri usulini bilib oling"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Yopish"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Parolni ko'rsatish"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} soat",
|
||||
"amount_minutes": "{{amount}} daqiqa",
|
||||
"last_time_played": "Oxirgi o'yin {{period}}",
|
||||
"activity": "So'nggi faollik",
|
||||
"library": "Kutubxona",
|
||||
"total_play_time": "Jami o'ynalgan vaqt",
|
||||
"no_recent_activity_title": "Hmm... Bu yerda hech narsa yo'q",
|
||||
"no_recent_activity_description": "Siz uzoq vaqtdan beri o'ynamagansiz. Buni o'zgartirish vaqti keldi!",
|
||||
"display_name": "Ko'rsatiladigan ism",
|
||||
"saving": "Saqlanmoqda",
|
||||
"save": "Saqlash",
|
||||
"edit_profile": "Profilni tahrirlash",
|
||||
"saved_successfully": "Muvaffaqiyatli saqlandi",
|
||||
"try_again": "Iltimos, qayta urinib ko'ring",
|
||||
"sign_out_modal_title": "Ishonchingiz komilmi?",
|
||||
"cancel": "Bekor qilish",
|
||||
"successfully_signed_out": "Tizimdan muvaffaqiyatli chiqdingiz",
|
||||
"sign_out": "Chiqish",
|
||||
"playing_for": "{{amount}} o'ynalgan",
|
||||
"sign_out_modal_text": "Sizning kutubxonangiz joriy hisob qaydnomangizga bog'langan. Tizimdan chiqsangiz, kutubxonangiz mavjud bo'lmaydi va progress saqlanmaydi. Chiqasizmi?",
|
||||
"add_friends": "Do'stlar qo'shish",
|
||||
"add": "Qo'shish",
|
||||
"friend_code": "Do'st kodi",
|
||||
"see_profile": "Profilni ko'rish",
|
||||
"sending": "Yuborilmoqda",
|
||||
"friend_request_sent": "Do'stlik so'rovi yuborildi",
|
||||
"friends": "Do'stlar",
|
||||
"friends_list": "Do'stlar ro'yxati",
|
||||
"user_not_found": "Foydalanuvchi topilmadi",
|
||||
"block_user": "Foydalanuvchini bloklash",
|
||||
"add_friend": "Do'st qo'shish",
|
||||
"request_sent": "So'rov yuborildi",
|
||||
"request_received": "So'rov qabul qilindi",
|
||||
"accept_request": "So'rovni qabul qilish",
|
||||
"ignore_request": "So'rovni e'tiborsiz qoldirish",
|
||||
"cancel_request": "So'rovni bekor qilish",
|
||||
"undo_friendship": "Do'stni o'chirish",
|
||||
"request_accepted": "So'rov qabul qilindi",
|
||||
"user_blocked_successfully": "Foydalanuvchi muvaffaqiyatli bloklandi",
|
||||
"user_block_modal_text": "{{displayName}} bloklanadi",
|
||||
"blocked_users": "Bloklangan foydalanuvchilar",
|
||||
"unblock": "Blokdan chiqarish",
|
||||
"no_friends_added": "Siz hali hech qanday do'st qo'shmagansiz",
|
||||
"pending": "Kutilmoqda",
|
||||
"no_pending_invites": "Sizda javob kutayotgan so'rovlar yo'q",
|
||||
"no_blocked_users": "Siz hech kimni bloklamagansiz",
|
||||
"friend_code_copied": "Do'st kodi nusxalandi",
|
||||
"displayname_max_length": "Ko'rsatiladigan ism 50 ta belgidan oshmasligi kerak",
|
||||
"displayname_min_length": "Ko'rsatiladigan ism kamida 3 ta belgidan iborat bo'lishi kerak",
|
||||
"image_process_failure": "Rasmni qayta ishlashda xatolik yuz berdi",
|
||||
"locked_profile": "Bu profil shaxsiy",
|
||||
"privacy_hint": "Uni kimlar ko'rishi mumkinligini belgilash uchun <0>Sozlamalar</0>ga o'ting",
|
||||
"profile_reported": "Profil haqida xabar berildi",
|
||||
"report": "Xabar berish",
|
||||
"report_description": "Qo'shimcha ma'lumot",
|
||||
"report_description_placeholder": "Qo'shimcha ma'lumot",
|
||||
"report_profile": "Bu profil haqida shikoyat qilish",
|
||||
"report_reason": "Nega bu profil haqida shikoyat qilyapsiz?",
|
||||
"report_reason_hate": "Nafrat qo'zg'atish",
|
||||
"report_reason_other": "Boshqa",
|
||||
"report_reason_sexual_content": "Jinsiy tarkib",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_violence": "Zo'ravonlik",
|
||||
"required_field": "Bu maydon to'ldirilishi shart",
|
||||
"undo_friendship_modal_text": "Bu {{displayName}} bilan do'stligingizni bekor qiladi",
|
||||
"your_friend_code": "Sizning do'st kodingiz:",
|
||||
"upload_banner": "Banner yuklash",
|
||||
"uploading_banner": "Banner yuklanmoqda...",
|
||||
"background_image_updated": "Fon rasmi yangilandi",
|
||||
"stats": "Statistika",
|
||||
"achievements": "Yutuqlar",
|
||||
"games": "O'yinlar",
|
||||
"top_percentile": "Yuqori {{percentile}}%",
|
||||
"ranking_updated_weekly": "Reyting har hafta yangilanadi",
|
||||
"playing": "{{game}}ni o'ynayapti",
|
||||
"achievements_unlocked": "Yutuqlar ochildi",
|
||||
"earned_points": "To'plangan ballar:",
|
||||
"show_achievements_on_profile": "Yutuqlaringizni profilingizda ko'rsating",
|
||||
"show_points_on_profile": "To'plangan ballarni profilingizda ko'rsating"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Yutuq ochildi",
|
||||
"user_achievements": "{{displayName}}ning yutuqlari",
|
||||
"your_achievements": "Sizning yutuqlaringiz",
|
||||
"unlocked_at": "Ochilgan sana: {{date}}",
|
||||
"subscription_needed": "Bu kontentni ko'rish uchun Hydra Cloud obunasi kerak",
|
||||
"new_achievements_unlocked": "{{gameCount}} o'yindan {{achievementCount}} ta yangi yutuq ochildi",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} yutuq",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} uchun {{achievementCount}} ta yangi yutuq ochildi",
|
||||
"hidden_achievement_tooltip": "Bu yashirin yutuq",
|
||||
"achievement_earn_points": "Bu yutuq bilan {{points}} ball to'plang",
|
||||
"earned_points": "To'plangan ballar:",
|
||||
"available_points": "Mavjud ballar:",
|
||||
"how_to_earn_achievements_points": "Yutuq ballarini qanday to'plash mumkin?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud obunasi",
|
||||
"subscribe_now": "Hoziroq obuna bo'ling",
|
||||
"cloud_saving": "Bulutli saqlash",
|
||||
"cloud_achievements": "Yutuqlaringizni bulutda saqlang",
|
||||
"animated_profile_picture": "Animatsiyali profil rasmi",
|
||||
"premium_support": "Premium qo'llab-quvvatlash",
|
||||
"show_and_compare_achievements": "Yutuqlaringizni boshqa foydalanuvchilarning yutuqlari bilan solishtiring va ko'rsating",
|
||||
"animated_profile_banner": "Animatsiyali profil banneri",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Siz hozirgina Hydra Cloud funksiyasini kashf etdingiz!",
|
||||
"learn_more": "Batafsil ma'lumot",
|
||||
"debrid_description": "Nimbus bilan 4 barobar tezroq yuklab oling"
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@
|
||||
"options": "选项",
|
||||
"executable_section_title": "可执行文件",
|
||||
"executable_section_description": "点击 \"Play\" 时将执行的文件的路径",
|
||||
"downloads_secion_title": "下载",
|
||||
"downloads_section_title": "下载",
|
||||
"downloads_section_description": "查看此游戏的更新或其他版本",
|
||||
"danger_zone_section_title": "危险操作",
|
||||
"danger_zone_section_description": "从您的库或Hydra下载的文件中删除此游戏",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { CloudSync } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameShop } from "@types";
|
||||
import i18next, { t } from "i18next";
|
||||
import { formatDate } from "date-fns";
|
||||
|
||||
const uploadSaveGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -10,16 +8,11 @@ const uploadSaveGame = async (
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null
|
||||
) => {
|
||||
const { language } = i18next;
|
||||
|
||||
return CloudSync.uploadSaveGame(
|
||||
objectId,
|
||||
shop,
|
||||
downloadOptionTitle,
|
||||
t("backup_from", {
|
||||
ns: "game_details",
|
||||
date: formatDate(new Date(), language),
|
||||
})
|
||||
CloudSync.getBackupLabel(false)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import kill from "kill-port";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import resources from "@locales";
|
||||
@@ -58,7 +57,7 @@ app.whenReady().then(async () => {
|
||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||
});
|
||||
|
||||
await kill(PythonRPC.RPC_PORT).finally(() => loadState());
|
||||
await loadState();
|
||||
|
||||
const language = await db.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
||||
import { Aria2, DownloadManager, Ludusavi, startMainLoop } from "./services";
|
||||
import { RealDebridClient } from "./services/download/real-debrid";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
@@ -20,6 +20,8 @@ export const loadState = async () => {
|
||||
|
||||
await import("./events");
|
||||
|
||||
Aria2.spawn();
|
||||
|
||||
if (userPreferences?.realDebridApiToken) {
|
||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||
}
|
||||
@@ -30,7 +32,7 @@ export const loadState = async () => {
|
||||
|
||||
Ludusavi.addManifestToLudusaviConfig();
|
||||
|
||||
HydraApi.setupApi().then(() => {
|
||||
await HydraApi.setupApi().then(() => {
|
||||
uploadGamesBatch();
|
||||
});
|
||||
|
||||
|
||||
28
src/main/services/aria2.ts
Normal file
28
src/main/services/aria2.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import path from "node:path";
|
||||
import cp from "node:child_process";
|
||||
import { app } from "electron";
|
||||
|
||||
export class Aria2 {
|
||||
private static process: cp.ChildProcess | null = null;
|
||||
|
||||
public static spawn() {
|
||||
const binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
|
||||
this.process = cp.spawn(
|
||||
binaryPath,
|
||||
[
|
||||
"--enable-rpc",
|
||||
"--rpc-listen-all",
|
||||
"--file-allocation=none",
|
||||
"--allow-overwrite=true",
|
||||
],
|
||||
{ stdio: "inherit", windowsHide: true }
|
||||
);
|
||||
}
|
||||
|
||||
public static kill() {
|
||||
this.process?.kill();
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,28 @@ import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
import { Ludusavi } from "./ludusavi";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { formatDate, SubscriptionRequiredError } from "@shared";
|
||||
import i18next, { t } from "i18next";
|
||||
|
||||
export class CloudSync {
|
||||
public static getBackupLabel(automatic: boolean) {
|
||||
const language = i18next.language;
|
||||
|
||||
const date = formatDate(new Date(), language);
|
||||
|
||||
if (automatic) {
|
||||
return t("automatic_backup_from", {
|
||||
ns: "game_details",
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
return t("backup_from", {
|
||||
ns: "game_details",
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
private static async bundleBackup(
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -25,7 +44,11 @@ export class CloudSync {
|
||||
|
||||
// Remove existing backup
|
||||
if (fs.existsSync(backupPath)) {
|
||||
fs.rmSync(backupPath, { recursive: true });
|
||||
try {
|
||||
await fs.promises.rm(backupPath, { recursive: true });
|
||||
} catch (error) {
|
||||
logger.error("Failed to remove backup path", error);
|
||||
}
|
||||
}
|
||||
|
||||
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||
@@ -101,11 +124,10 @@ export class CloudSync {
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(bundleLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to remove tar file", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
try {
|
||||
await fs.promises.unlink(bundleLocation);
|
||||
} catch (error) {
|
||||
logger.error("Failed to remove tar file", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ export class DownloadManager {
|
||||
case Downloader.RealDebrid: {
|
||||
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
||||
|
||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedInRealDebrid);
|
||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
@@ -395,7 +395,7 @@ export class DownloadManager {
|
||||
download.uri
|
||||
);
|
||||
|
||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedInHydra);
|
||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from "./cloud-sync";
|
||||
export * from "./7zip";
|
||||
export * from "./game-files-manager";
|
||||
export * from "./common-redist-manager";
|
||||
export * from "./aria2";
|
||||
|
||||
@@ -6,9 +6,7 @@ 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 ""}'`,
|
||||
@@ -229,17 +227,12 @@ 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),
|
||||
})
|
||||
CloudSync.getBackupLabel(true)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -298,8 +291,6 @@ const onCloseGame = (game: Game) => {
|
||||
)!;
|
||||
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
|
||||
|
||||
const { language } = i18next;
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
@@ -312,10 +303,7 @@ const onCloseGame = (game: Game) => {
|
||||
game.objectId,
|
||||
game.shop,
|
||||
null,
|
||||
t("automatic_backup_from", {
|
||||
ns: "game_details",
|
||||
date: formatDate(new Date(), language),
|
||||
})
|
||||
CloudSync.getBackupLabel(true)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -372,12 +372,12 @@ export class WindowManager {
|
||||
|
||||
const sortedGames = sortBy(filteredGames, "lastTimePlayed", "DESC");
|
||||
|
||||
return slice(sortedGames, 5);
|
||||
return slice(sortedGames, 0, 5);
|
||||
});
|
||||
|
||||
const recentlyPlayedGames: Array<MenuItemConstructorOptions | MenuItem> =
|
||||
games.map(({ title, executablePath }) => ({
|
||||
label: title.length > 15 ? `${title.slice(0, 15)}…` : title,
|
||||
label: title.length > 18 ? `${title.slice(0, 18)}…` : title,
|
||||
type: "normal",
|
||||
click: async () => {
|
||||
if (!executablePath) return;
|
||||
@@ -418,7 +418,10 @@ export class WindowManager {
|
||||
},
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
if (process.platform === "linux") {
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
return contextMenu;
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -85,5 +85,6 @@
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
line-height: 1.15;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { formatDate, getDateLocale } from "@shared";
|
||||
import { format, formatDistance, subMilliseconds } from "date-fns";
|
||||
import type { FormatDistanceOptions } from "date-fns";
|
||||
import { enUS } from "date-fns/locale";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function useDate() {
|
||||
@@ -41,10 +40,10 @@ export function useDate() {
|
||||
},
|
||||
|
||||
formatDateTime: (date: number | Date | string): string => {
|
||||
const locale = getDateLocale(language);
|
||||
return format(
|
||||
date,
|
||||
locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy HH:mm"
|
||||
language == "en" ? "MM-dd-yyyy - hh:mm a" : "dd/MM/yyyy HH:mm",
|
||||
{ locale: getDateLocale(language) }
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from "react";
|
||||
|
||||
enum Feature {
|
||||
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
|
||||
Torbox = "TORBOX",
|
||||
TorBox = "TORBOX",
|
||||
Nimbus = "NIMBUS",
|
||||
NimbusPreview = "NIMBUS_PREVIEW",
|
||||
}
|
||||
|
||||
@@ -73,8 +73,11 @@
|
||||
min-height: 140px;
|
||||
max-height: 140px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--hydra {
|
||||
box-shadow: 0px 0px 16px 0px rgba(12, 241, 202, 0.15);
|
||||
}
|
||||
}
|
||||
&__cover {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
@@ -145,4 +148,14 @@
|
||||
padding: 8px;
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
&__hydra-gradient {
|
||||
background: linear-gradient(90deg, #01483c 0%, #0cf1ca 50%, #01483c 100%);
|
||||
box-shadow: 0px 0px 8px 0px rgba(12, 241, 202, 0.15);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import cn from "classnames";
|
||||
|
||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||
|
||||
@@ -32,8 +33,6 @@ import {
|
||||
XCircleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
|
||||
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
||||
|
||||
export interface DownloadGroupProps {
|
||||
library: LibraryGame[];
|
||||
title: string;
|
||||
@@ -310,7 +309,13 @@ export function DownloadGroup({
|
||||
<ul className="download-group__downloads">
|
||||
{library.map((game) => {
|
||||
return (
|
||||
<li key={game.id} className="download-group__item">
|
||||
<li
|
||||
key={game.id}
|
||||
className={cn("download-group__item", {
|
||||
"download-group__item--hydra":
|
||||
game.download?.downloader === Downloader.Hydra,
|
||||
})}
|
||||
>
|
||||
<div className="download-group__cover">
|
||||
<div className="download-group__cover-backdrop">
|
||||
<img
|
||||
@@ -320,20 +325,7 @@ export function DownloadGroup({
|
||||
/>
|
||||
|
||||
<div className="download-group__cover-content">
|
||||
{game.download?.downloader === Downloader.TorBox ? (
|
||||
<Badge>
|
||||
<img
|
||||
src={torBoxLogo}
|
||||
alt="TorBox"
|
||||
style={{ width: 13 }}
|
||||
/>
|
||||
<span>TorBox</span>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge>
|
||||
{DOWNLOADER_NAME[game.download!.downloader]}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,18 +368,7 @@ export function DownloadGroup({
|
||||
</div>
|
||||
|
||||
{game.download?.downloader === Downloader.Hydra && (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, #01483C 0%, #0CF1CA 50%, #01483C 100%)",
|
||||
boxShadow: "0px 0px 8px 0px rgba(12, 241, 202, 0.15)",
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
height: 2,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
<div className="download-group__hydra-gradient" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -96,14 +96,14 @@ export function DownloadSettingsModal({
|
||||
return Downloader.Hydra;
|
||||
}
|
||||
|
||||
if (availableDownloaders.includes(Downloader.TorBox)) {
|
||||
return Downloader.TorBox;
|
||||
}
|
||||
|
||||
if (availableDownloaders.includes(Downloader.RealDebrid)) {
|
||||
return Downloader.RealDebrid;
|
||||
}
|
||||
|
||||
if (availableDownloaders.includes(Downloader.TorBox)) {
|
||||
return Downloader.TorBox;
|
||||
}
|
||||
|
||||
return availableDownloaders[0];
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -362,7 +362,7 @@ export function GameOptionsModal({
|
||||
|
||||
<div className="game-options-modal__downloads">
|
||||
<div className="game-options-modal__header">
|
||||
<h2>{t("downloads_secion_title")}</h2>
|
||||
<h2>{t("downloads_section_title")}</h2>
|
||||
<h4 className="game-options-modal__header-description">
|
||||
{t("downloads_section_description")}
|
||||
</h4>
|
||||
|
||||
@@ -16,7 +16,7 @@ const TORBOX_URL = torBoxReferralCode
|
||||
: "https://torbox.app";
|
||||
const TORBOX_API_TOKEN_URL = "https://torbox.app/settings";
|
||||
|
||||
export function SettingsTorbox() {
|
||||
export function SettingsTorBox() {
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||
import { SettingsGeneral } from "./settings-general";
|
||||
import { SettingsBehavior } from "./settings-behavior";
|
||||
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
||||
import { SettingsDownloadSources } from "./settings-download-sources";
|
||||
import {
|
||||
SettingsContextConsumer,
|
||||
@@ -14,7 +13,7 @@ import { useFeature, useUserDetails } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
import "./settings.scss";
|
||||
import { SettingsAppearance } from "./aparence/settings-appearance";
|
||||
import { SettingsTorbox } from "./settings-torbox";
|
||||
import { SettingsTorBox } from "./settings-torbox";
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation("settings");
|
||||
@@ -23,7 +22,7 @@ export default function Settings() {
|
||||
|
||||
const { isFeatureEnabled, Feature } = useFeature();
|
||||
|
||||
const isTorboxEnabled = isFeatureEnabled(Feature.Torbox);
|
||||
const isTorBoxEnabled = isFeatureEnabled(Feature.TorBox);
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const categories = [
|
||||
@@ -34,19 +33,10 @@ export default function Settings() {
|
||||
tabLabel: t("appearance"),
|
||||
contentTitle: t("appearance"),
|
||||
},
|
||||
...(isTorboxEnabled
|
||||
...(isTorBoxEnabled
|
||||
? [
|
||||
{
|
||||
tabLabel: (
|
||||
<>
|
||||
<img
|
||||
src={torBoxLogo}
|
||||
alt="TorBox"
|
||||
style={{ width: 13, height: 13 }}
|
||||
/>{" "}
|
||||
Torbox
|
||||
</>
|
||||
),
|
||||
tabLabel: "TorBox",
|
||||
contentTitle: "TorBox",
|
||||
},
|
||||
]
|
||||
@@ -60,7 +50,7 @@ export default function Settings() {
|
||||
{ tabLabel: t("account"), contentTitle: t("account") },
|
||||
];
|
||||
return categories;
|
||||
}, [userDetails, t, isTorboxEnabled]);
|
||||
}, [userDetails, t, isTorBoxEnabled]);
|
||||
|
||||
return (
|
||||
<SettingsContextProvider>
|
||||
@@ -84,7 +74,7 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 4) {
|
||||
return <SettingsTorbox />;
|
||||
return <SettingsTorBox />;
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 5) {
|
||||
|
||||
@@ -53,11 +53,11 @@ export enum AuthPage {
|
||||
}
|
||||
|
||||
export enum DownloadError {
|
||||
NotCachedInRealDebrid = "download_error_not_cached_in_real_debrid",
|
||||
NotCachedInTorbox = "download_error_not_cached_in_torbox",
|
||||
NotCachedOnRealDebrid = "download_error_not_cached_on_real_debrid",
|
||||
NotCachedOnTorBox = "download_error_not_cached_on_torbox",
|
||||
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
|
||||
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
|
||||
NotCachedInHydra = "download_error_not_cached_in_hydra",
|
||||
NotCachedOnHydra = "download_error_not_cached_on_hydra",
|
||||
}
|
||||
|
||||
export const FILE_EXTENSIONS_TO_EXTRACT = [".rar", ".zip", ".7z"];
|
||||
|
||||
@@ -173,7 +173,5 @@ export const formatDate = (
|
||||
language: string
|
||||
): string => {
|
||||
if (isNaN(new Date(date).getDate())) return "N/A";
|
||||
|
||||
const locale = getDateLocale(language);
|
||||
return format(date, locale == enUS ? "MM/dd/yyyy" : "dd/MM/yyyy");
|
||||
return format(date, language == "en" ? "MM-dd-yyyy" : "dd/MM/yyyy");
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface DownloadProgress {
|
||||
download: Download;
|
||||
}
|
||||
|
||||
/* Torbox */
|
||||
/* TorBox */
|
||||
export interface TorBoxUser {
|
||||
id: number;
|
||||
email: string;
|
||||
|
||||
30
yarn.lock
30
yarn.lock
@@ -1460,9 +1460,9 @@
|
||||
optionalDependencies:
|
||||
global-agent "^3.0.0"
|
||||
|
||||
"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2":
|
||||
"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2":
|
||||
version "10.2.0-electron.1"
|
||||
resolved "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2"
|
||||
resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
|
||||
dependencies:
|
||||
env-paths "^2.2.0"
|
||||
exponential-backoff "^3.1.1"
|
||||
@@ -4501,10 +4501,10 @@ crc@^3.8.0:
|
||||
dependencies:
|
||||
buffer "^5.1.0"
|
||||
|
||||
create-desktop-shortcuts@^1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/create-desktop-shortcuts/-/create-desktop-shortcuts-1.11.0.tgz#8eed89329e9bce70dece46d02a80573fe1f2536d"
|
||||
integrity sha512-nmVtPVqNyMuAyMpDnd7l++hb2laqCWZXnHQaFhqGT1YEi2Ve3unu6QyuyIpGxAwIscNHcG1Ehnl+lFw6ygB2nQ==
|
||||
create-desktop-shortcuts@^1.11.1:
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/create-desktop-shortcuts/-/create-desktop-shortcuts-1.11.1.tgz#59f9dced7931bda551c0717791a909419472c809"
|
||||
integrity sha512-EiHvxrMXXEp4xDD3Nvu1FKLueL9+aBWFYtuTlstYZLIg9H45SoYciizteNB+hvQAls3cRSpoXCM7c4q+lcJpyw==
|
||||
dependencies:
|
||||
which "2.0.2"
|
||||
|
||||
@@ -5814,11 +5814,6 @@ get-symbol-description@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
|
||||
get-them-args@1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/get-them-args/-/get-them-args-1.3.2.tgz#74a20ba8a4abece5ae199ad03f2bcc68fdfc9ba5"
|
||||
integrity sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==
|
||||
|
||||
git-raw-commits@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-4.0.0.tgz#b212fd2bff9726d27c1283a1157e829490593285"
|
||||
@@ -6826,14 +6821,6 @@ keyv@^4.0.0, keyv@^4.5.3:
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
kill-port@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/kill-port/-/kill-port-2.0.1.tgz#e5e18e2706b13d54320938be42cb7d40609b15cf"
|
||||
integrity sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==
|
||||
dependencies:
|
||||
get-them-args "1.3.2"
|
||||
shell-exec "1.0.2"
|
||||
|
||||
language-subtag-registry@^0.3.20:
|
||||
version "0.3.23"
|
||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"
|
||||
@@ -8459,11 +8446,6 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-exec@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756"
|
||||
integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==
|
||||
|
||||
side-channel-list@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
||||
|
||||
Reference in New Issue
Block a user