Compare commits

..

4 Commits

Author SHA1 Message Date
Moyasee
f0f272c162 fix: handling exceptions 2025-11-04 22:25:32 +02:00
Moyasee
363e52cdb6 fix: duplication 2025-11-04 22:22:02 +02:00
Moyasee
e04a94d10d fix: duplacation and formatting 2025-11-04 22:10:07 +02:00
Moyasee
6733a3e5b0 feat: ability to crop/resize picture before applying 2025-11-04 21:50:14 +02:00
264 changed files with 5030 additions and 17674 deletions

View File

@@ -28,26 +28,6 @@
- Use async/await instead of promises when possible - Use async/await instead of promises when possible
- Prefer named exports over default exports for utilities and services - Prefer named exports over default exports for utilities and services
## ESLint Issues
- **Always try to fix ESLint errors properly before disabling rules**
- When encountering ESLint errors, explore these solutions in order:
1. **Fix the code to comply with the rule** (e.g., add missing required elements, fix accessibility issues)
2. **Use minimal markup to satisfy the rule** (e.g., add empty `<track>` elements for videos without captions, add `role` attributes)
3. **Only disable the rule as a last resort** when no reasonable solution exists
- When disabling a rule, always include a comment explaining why it's necessary
- Examples of proper fixes:
- For `jsx-a11y/media-has-caption`: Add `<track kind="captions" />` even if no captions are available
- For `jsx-a11y/alt-text`: Add meaningful alt text or `alt=""` for decorative images
- For accessibility rules: Add appropriate ARIA attributes rather than disabling
## TypeScript Array Syntax
- **Always use `T[]` syntax instead of `Array<T>`** for array types
- Prefer: `string[]`, `number[]`, `MyType[]`
- Avoid: `Array<string>`, `Array<number>`, `Array<MyType>`
- This applies to all type annotations, type assertions, and generic type parameters
## Comments ## Comments
- Keep comments concise and purposeful; avoid verbose explanations. - Keep comments concise and purposeful; avoid verbose explanations.

View File

@@ -1,7 +1,6 @@
MAIN_VITE_API_URL= MAIN_VITE_API_URL=
MAIN_VITE_AUTH_URL= MAIN_VITE_AUTH_URL=
MAIN_VITE_WS_URL= MAIN_VITE_WS_URL=
MAIN_VITE_NIMBUS_API_URL=
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID= RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
RENDERER_VITE_TORBOX_REFERRAL_CODE= RENDERER_VITE_TORBOX_REFERRAL_CODE=
MAIN_VITE_LAUNCHER_SUBDOMAIN= MAIN_VITE_LAUNCHER_SUBDOMAIN=

65
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Bug Report
description: Create a report to help us improve. Write in English.
title: "[BUG] Write a title for your bug"
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thank you for creating a bug report to help us improve!
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: bug-reproduce
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior. For example, "1. Go to '...', 2. Click on '...', 3. See error"
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
id: additional-info
attributes:
label: Additional information and data
description: |
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
id: OS
attributes:
label: Operating System
description: Which operating system are you using (e.g., Windows 11/Linux Distro/Steam Deck)?
validations:
required: true
- type: input
id: hydra-version
attributes:
label: Hydra Version
description: Please provide the version of Hydra you are using.
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Before opening this Issue
options:
- label: I have searched the issues of this repository and believe that this is not a duplicate.
required: true
- label: I am aware that Hydra team does not offer any support or help regarding the downloaded games.
required: true
- label: I have read the [Frequently Asked Questions (FAQ)](https://github.com/hydralauncher/hydra/wiki/FAQ).
required: true

View File

@@ -0,0 +1,37 @@
name: Feature Request
description: Request a new feature.
title: "[REQUEST] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to suggest a new feature!
- type: textarea
id: problem-related
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

View File

@@ -2,9 +2,11 @@
**When submitting this pull request, I confirm the following (please check the boxes):** **When submitting this pull request, I confirm the following (please check the boxes):**
- [ ] I have read the [Hydra documentation](https://docs.hydralauncher.gg/getting-started.html). - [ ] I have read and understood the [Contributor Guidelines](https://github.com/hydralauncher/hydra?tab=readme-ov-file#ways-you-can-contribute).
- [ ] I have checked that there are no duplicate pull requests related to this request. - [ ] I have checked that there are no duplicate pull requests related to this request.
- [ ] I have considered, and confirm that this submission is valuable to others. - [ ] I have considered, and confirm that this submission is valuable to others.
- [ ] I accept that this submission may not be used and the pull request may be closed at the discretion of the maintainers. - [ ] I accept that this submission may not be used and the pull request may be closed at the discretion of the maintainers.
**Fill in the PR content:** **Fill in the PR content:**
-

View File

@@ -42,7 +42,6 @@ jobs:
run: yarn build run: yarn build
env: env:
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- name: Deploy to Cloudflare Pages - name: Deploy to Cloudflare Pages
env: env:

View File

@@ -2,9 +2,6 @@ name: Build
on: on:
pull_request: pull_request:
push:
branches:
- main
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -57,7 +54,6 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }} MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -74,7 +70,6 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }} MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -54,10 +54,9 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
@@ -72,10 +71,9 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
MAIN_VITE_NIMBUS_API_URL: ${{ vars.MAIN_VITE_NIMBUS_API_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }} MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}

View File

@@ -137,7 +137,7 @@ jobs:
if git diff --staged --quiet; then if git diff --staged --quiet; then
echo "No changes to commit" echo "No changes to commit"
else else
COMMIT_MSG="${{ steps.get-version.outputs.version }}" COMMIT_MSG="v${{ steps.get-version.outputs.version }}"
git commit -m "$COMMIT_MSG" git commit -m "$COMMIT_MSG"

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
[<img src="https://raw.githubusercontent.com/hydralauncher/hydra/refs/heads/main/resources/icon.png" width="144"/>](https://help.hydralauncher.gg) [<img src="./resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
<h1 align="center">Hydra Launcher</h1> <h1 align="center">Hydra Launcher</h1>
@@ -10,7 +10,6 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases) [![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![chocolatey](https://img.shields.io/chocolatey/v/hydralauncher.svg)](https://community.chocolatey.org/packages/hydralauncher)
![Hydra Launcher Home Page](./docs/screenshot.png) ![Hydra Launcher Home Page](./docs/screenshot.png)

View File

@@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "3.8.1", "version": "3.7.4",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",
@@ -40,7 +40,6 @@
"@primer/octicons-react": "^19.9.0", "@primer/octicons-react": "^19.9.0",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "^2.2.3",
"@sentry/react": "^10.33.0",
"@tiptap/extension-bold": "^3.6.2", "@tiptap/extension-bold": "^3.6.2",
"@tiptap/extension-italic": "^3.6.2", "@tiptap/extension-italic": "^3.6.2",
"@tiptap/extension-link": "^3.6.2", "@tiptap/extension-link": "^3.6.2",
@@ -64,15 +63,12 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"file-type": "^20.5.0", "file-type": "^20.5.0",
"framer-motion": "^12.15.0", "framer-motion": "^12.15.0",
"get-port": "^7.1.0",
"hls.js": "^1.5.12",
"i18next": "^23.11.2", "i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1", "i18next-browser-languagedetector": "^7.2.1",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"node-7z": "^3.0.0",
"parse-torrent": "^11.0.18", "parse-torrent": "^11.0.18",
"rc-virtual-list": "^3.18.3", "rc-virtual-list": "^3.18.3",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
@@ -88,12 +84,11 @@
"sound-play": "^1.1.0", "sound-play": "^1.1.0",
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor", "steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
"sudo-prompt": "^9.2.1", "sudo-prompt": "^9.2.1",
"tar": "^7.5.4", "tar": "^7.4.3",
"tough-cookie": "^5.1.1", "tough-cookie": "^5.1.1",
"user-agents": "^1.1.387", "user-agents": "^1.1.387",
"uuid": "^13.0.0", "uuid": "^13.0.0",
"winreg": "^1.2.5", "winreg": "^1.2.5",
"workwonders-sdk": "0.1.1",
"ws": "^8.18.1", "ws": "^8.18.1",
"yaml": "^2.6.1", "yaml": "^2.6.1",
"yup": "^1.5.0" "yup": "^1.5.0"

2
proto

Submodule proto updated: 6f11c99c57...7a23620f93

View File

@@ -1,5 +1,4 @@
import aria2p import aria2p
from aria2p.client import ClientException as DownloadNotFound
class HttpDownloader: class HttpDownloader:
def __init__(self): def __init__(self):
@@ -12,16 +11,12 @@ class HttpDownloader:
) )
) )
def start_download(self, url: str, save_path: str, header, out: str = None): def start_download(self, url: str, save_path: str, header: str, out: str = None):
if self.download: if self.download:
self.aria2.resume([self.download]) self.aria2.resume([self.download])
else: else:
options = {"dir": save_path} downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
if header:
options["header"] = header
if out:
options["out"] = out
downloads = self.aria2.add(url, options=options)
self.download = downloads[0] self.download = downloads[0]
def pause_download(self): def pause_download(self):
@@ -37,11 +32,7 @@ class HttpDownloader:
if self.download == None: if self.download == None:
return None return None
try: download = self.aria2.get_download(self.download.gid)
download = self.aria2.get_download(self.download.gid)
except DownloadNotFound:
self.download = None
return None
response = { response = {
'folderName': download.name, 'folderName': download.name,

View File

@@ -0,0 +1,151 @@
import aria2p
from aria2p.client import ClientException as DownloadNotFound
class HttpMultiLinkDownloader:
def __init__(self):
self.downloads = []
self.completed_downloads = []
self.total_size = None
self.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
def start_download(self, urls: list[str], save_path: str, header: str = None, out: str = None, total_size: int = None):
"""Add multiple URLs to download queue with same options"""
options = {"dir": save_path}
if header:
options["header"] = header
if out:
options["out"] = out
# Clear any existing downloads first
self.cancel_download()
self.completed_downloads = []
self.total_size = total_size
for url in urls:
try:
added_downloads = self.aria2.add(url, options=options)
self.downloads.extend(added_downloads)
except Exception as e:
print(f"Error adding download for URL {url}: {str(e)}")
def pause_download(self):
"""Pause all active downloads"""
if self.downloads:
try:
self.aria2.pause(self.downloads)
except Exception as e:
print(f"Error pausing downloads: {str(e)}")
def cancel_download(self):
"""Cancel and remove all downloads"""
if self.downloads:
try:
# First try to stop the downloads
self.aria2.remove(self.downloads)
except Exception as e:
print(f"Error removing downloads: {str(e)}")
finally:
# Clear the downloads list regardless of success/failure
self.downloads = []
self.completed_downloads = []
def get_download_status(self):
"""Get status for all tracked downloads, auto-remove completed/failed ones"""
if not self.downloads and not self.completed_downloads:
return []
total_completed = 0
current_download_speed = 0
active_downloads = []
to_remove = []
# First calculate sizes from completed downloads
for completed in self.completed_downloads:
total_completed += completed['size']
# Then check active downloads
for download in self.downloads:
try:
current_download = self.aria2.get_download(download.gid)
# Skip downloads that are not properly initialized
if not current_download or not current_download.files:
to_remove.append(download)
continue
# Add to completed size and speed calculations
total_completed += current_download.completed_length
current_download_speed += current_download.download_speed
# If download is complete, move it to completed_downloads
if current_download.status == 'complete':
self.completed_downloads.append({
'name': current_download.name,
'size': current_download.total_length
})
to_remove.append(download)
else:
active_downloads.append({
'name': current_download.name,
'size': current_download.total_length,
'completed': current_download.completed_length,
'speed': current_download.download_speed
})
except DownloadNotFound:
to_remove.append(download)
continue
except Exception as e:
print(f"Error getting download status: {str(e)}")
continue
# Clean up completed/removed downloads from active list
for download in to_remove:
try:
if download in self.downloads:
self.downloads.remove(download)
except ValueError:
pass
# Return aggregate status
if self.total_size or active_downloads or self.completed_downloads:
# Use the first active download's name as the folder name, or completed if none active
folder_name = None
if active_downloads:
folder_name = active_downloads[0]['name']
elif self.completed_downloads:
folder_name = self.completed_downloads[0]['name']
if folder_name and '/' in folder_name:
folder_name = folder_name.split('/')[0]
# Use provided total size if available, otherwise sum from downloads
total_size = self.total_size
if not total_size:
total_size = sum(d['size'] for d in active_downloads) + sum(d['size'] for d in self.completed_downloads)
# Calculate completion status based on total downloaded vs total size
is_complete = len(active_downloads) == 0 and total_completed >= (total_size * 0.99) # Allow 1% margin for size differences
# If all downloads are complete, clear the completed_downloads list to prevent status updates
if is_complete:
self.completed_downloads = []
return [{
'folderName': folder_name,
'fileSize': total_size,
'progress': total_completed / total_size if total_size > 0 else 0,
'downloadSpeed': current_download_speed,
'numPeers': 0,
'numSeeds': 0,
'status': 'complete' if is_complete else 'active',
'bytesDownloaded': total_completed,
}]
return []

View File

@@ -3,6 +3,7 @@ import sys, json, urllib.parse, psutil
from torrent_downloader import TorrentDownloader from torrent_downloader import TorrentDownloader
from http_downloader import HttpDownloader from http_downloader import HttpDownloader
from profile_image_processor import ProfileImageProcessor from profile_image_processor import ProfileImageProcessor
from http_multi_link_downloader import HttpMultiLinkDownloader
import libtorrent as lt import libtorrent as lt
app = Flask(__name__) app = Flask(__name__)
@@ -24,7 +25,15 @@ if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload)) initial_download = json.loads(urllib.parse.unquote(start_download_payload))
downloading_game_id = initial_download['game_id'] downloading_game_id = initial_download['game_id']
if initial_download['url'].startswith('magnet'): if isinstance(initial_download['url'], list):
# Handle multiple URLs using HttpMultiLinkDownloader
http_multi_downloader = HttpMultiLinkDownloader()
downloads[initial_download['game_id']] = http_multi_downloader
try:
http_multi_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
except Exception as e:
print("Error starting multi-link download", e)
elif initial_download['url'].startswith('magnet'):
torrent_downloader = TorrentDownloader(torrent_session) torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader downloads[initial_download['game_id']] = torrent_downloader
try: try:
@@ -69,6 +78,14 @@ def status():
if not status: if not status:
return jsonify(None) return jsonify(None)
if isinstance(status, list):
if not status: # Empty list
return jsonify(None)
# For multi-link downloader, use the aggregated status
# The status will already be aggregated by the HttpMultiLinkDownloader
return jsonify(status[0]), 200
return jsonify(status), 200 return jsonify(status), 200
@app.route("/seed-status", methods=["GET"]) @app.route("/seed-status", methods=["GET"])
@@ -87,7 +104,21 @@ def seed_status():
if not response: if not response:
continue continue
if response.get('status') == 5: # Torrent seeding check if isinstance(response, list):
# For multi-link downloader, check if all files are complete
if response and all(item['status'] == 'complete' for item in response):
seed_status.append({
'gameId': game_id,
'status': 'complete',
'folderName': response[0]['folderName'],
'fileSize': sum(item['fileSize'] for item in response),
'bytesDownloaded': sum(item['bytesDownloaded'] for item in response),
'downloadSpeed': 0,
'numPeers': 0,
'numSeeds': 0,
'progress': 1.0
})
elif response.get('status') == 5: # Original torrent seeding check
seed_status.append({ seed_status.append({
'gameId': game_id, 'gameId': game_id,
**response, **response,
@@ -122,11 +153,8 @@ def profile_image():
data = request.get_json() data = request.get_json()
image_path = data.get('image_path') image_path = data.get('image_path')
# use webp as default value for target_extension
target_extension = data.get('target_extension') or 'webp'
try: try:
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path, target_extension) processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200 return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
@@ -149,7 +177,15 @@ def action():
existing_downloader = downloads.get(game_id) existing_downloader = downloads.get(game_id)
if url.startswith('magnet'): if isinstance(url, list):
# Handle multiple URLs using HttpMultiLinkDownloader
if existing_downloader and isinstance(existing_downloader, HttpMultiLinkDownloader):
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
else:
http_multi_downloader = HttpMultiLinkDownloader()
downloads[game_id] = http_multi_downloader
http_multi_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
elif url.startswith('magnet'):
if existing_downloader and isinstance(existing_downloader, TorrentDownloader): if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
existing_downloader.start_download(url, data['save_path']) existing_downloader.start_download(url, data['save_path'])
else: else:

View File

@@ -4,7 +4,7 @@ import os, uuid, tempfile
class ProfileImageProcessor: class ProfileImageProcessor:
@staticmethod @staticmethod
def get_parsed_image_data(image_path, target_extension): def get_parsed_image_data(image_path):
Image.MAX_IMAGE_PIXELS = 933120000 Image.MAX_IMAGE_PIXELS = 933120000
image = Image.open(image_path) image = Image.open(image_path)
@@ -16,7 +16,7 @@ class ProfileImageProcessor:
return image_path, mime_type return image_path, mime_type
else: else:
new_uuid = str(uuid.uuid4()) new_uuid = str(uuid.uuid4())
new_image_path = os.path.join(tempfile.gettempdir(), new_uuid) + "." + target_extension new_image_path = os.path.join(tempfile.gettempdir(), new_uuid) + ".webp"
image.save(new_image_path) image.save(new_image_path)
new_image = Image.open(new_image_path) new_image = Image.open(new_image_path)
@@ -26,5 +26,5 @@ class ProfileImageProcessor:
@staticmethod @staticmethod
def process_image(image_path, target_extension): def process_image(image_path):
return ProfileImageProcessor.get_parsed_image_data(image_path, target_extension) return ProfileImageProcessor.get_parsed_image_data(image_path)

View File

@@ -13,7 +13,6 @@
}, },
"sidebar": { "sidebar": {
"catalogue": "Catalogue", "catalogue": "Catalogue",
"library": "Library",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Settings", "settings": "Settings",
"my_library": "My library", "my_library": "My library",
@@ -26,7 +25,6 @@
"game_has_no_executable": "Game has no executable selected", "game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in", "sign_in": "Sign in",
"friends": "Friends", "friends": "Friends",
"notifications": "Notifications",
"need_help": "Need help?", "need_help": "Need help?",
"favorites": "Favorites", "favorites": "Favorites",
"playable_button_title": "Show only games you can play now", "playable_button_title": "Show only games you can play now",
@@ -94,31 +92,13 @@
}, },
"header": { "header": {
"search": "Search games", "search": "Search games",
"search_library": "Search library",
"recent_searches": "Recent Searches",
"suggestions": "Suggestions",
"clear_history": "Clear",
"remove_from_history": "Remove from history",
"loading": "Loading...",
"no_results": "No results",
"home": "Home", "home": "Home",
"catalogue": "Catalogue", "catalogue": "Catalogue",
"library": "Library",
"downloads": "Downloads", "downloads": "Downloads",
"search_results": "Search results", "search_results": "Search results",
"settings": "Settings", "settings": "Settings",
"version_available_install": "Version {{version}} available. Click here to restart and install.", "version_available_install": "Version {{version}} available. Click here to restart and install.",
"version_available_download": "Version {{version}} available. Click here to download.", "version_available_download": "Version {{version}} available. Click here to download."
"scan_games_tooltip": "Scan PC for installed games",
"scan_games_title": "Scan PC for installed games",
"scan_games_description": "This will scan your disks for known game executables. This may take several minutes.",
"scan_games_start": "Start Scan",
"scan_games_cancel": "Cancel",
"scan_games_result": "Found {{found}} of {{total}} games without executable path",
"scan_games_no_results": "We couldn't find any installed games.",
"scan_games_in_progress": "Scanning your disks for installed games...",
"scan_games_close": "Close",
"scan_games_scan_again": "Scan Again"
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "No downloads in progress", "no_downloads_in_progress": "No downloads in progress",
@@ -126,7 +106,6 @@
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}", "downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…", "calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
"checking_files": "Checking {{title}} files… ({{percentage}} complete)", "checking_files": "Checking {{title}} files… ({{percentage}} complete)",
"extracting": "Extracting {{title}}… ({{percentage}} complete)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Installation complete", "installation_complete": "Installation complete",
"installation_complete_message": "Common redistributables installed successfully" "installation_complete_message": "Common redistributables installed successfully"
@@ -185,8 +164,6 @@
"repacks_modal_description": "Choose the repack you want to download", "repacks_modal_description": "Choose the repack you want to download",
"select_folder_hint": "To change the default folder, go to the <0>Settings</0>", "select_folder_hint": "To change the default folder, go to the <0>Settings</0>",
"download_now": "Download now", "download_now": "Download now",
"add_to_queue": "Add to queue",
"loading": "Loading...",
"no_shop_details": "Could not retrieve shop details.", "no_shop_details": "Could not retrieve shop details.",
"download_options": "Download options", "download_options": "Download options",
"download_path": "Download path", "download_path": "Download path",
@@ -196,12 +173,6 @@
"open_screenshot": "Open screenshot {{number}}", "open_screenshot": "Open screenshot {{number}}",
"download_settings": "Download settings", "download_settings": "Download settings",
"downloader": "Downloader", "downloader": "Downloader",
"downloader_online": "Online",
"downloader_not_configured": "Available but not configured",
"downloader_offline": "Link is offline",
"downloader_not_available": "Not available",
"recommended": "Recommended",
"go_to_settings": "Go to Settings",
"select_executable": "Select", "select_executable": "Select",
"no_executable_selected": "No executable selected", "no_executable_selected": "No executable selected",
"open_folder": "Open folder", "open_folder": "Open folder",
@@ -222,9 +193,7 @@
"danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra", "danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra",
"download_in_progress": "Download in progress", "download_in_progress": "Download in progress",
"download_paused": "Download paused", "download_paused": "Download paused",
"extracting": "Extracting",
"last_downloaded_option": "Last downloaded option", "last_downloaded_option": "Last downloaded option",
"new_download_option": "New",
"create_steam_shortcut": "Create Steam shortcut", "create_steam_shortcut": "Create Steam shortcut",
"create_shortcut_success": "Shortcut created successfully", "create_shortcut_success": "Shortcut created successfully",
"you_might_need_to_restart_steam": "You might need to restart Steam to see the changes", "you_might_need_to_restart_steam": "You might need to restart Steam to see the changes",
@@ -383,9 +352,6 @@
"audio": "Audio", "audio": "Audio",
"filter_by_source": "Filter by source", "filter_by_source": "Filter by source",
"no_repacks_found": "No sources found for this game", "no_repacks_found": "No sources found for this game",
"source_online": "Source is online",
"source_partial": "Some links are offline",
"source_offline": "Source is offline",
"delete_review": "Delete review", "delete_review": "Delete review",
"remove_review": "Remove Review", "remove_review": "Remove Review",
"delete_review_modal_title": "Are you sure you want to delete your review?", "delete_review_modal_title": "Are you sure you want to delete your review?",
@@ -418,10 +384,6 @@
"completed": "Completed", "completed": "Completed",
"removed": "Not downloaded", "removed": "Not downloaded",
"cancel": "Cancel", "cancel": "Cancel",
"cancel_download": "Cancel download?",
"cancel_download_description": "Are you sure you want to cancel this download? All downloaded files will be deleted.",
"keep_downloading": "No, keep downloading",
"yes_cancel": "Yes, cancel",
"filter": "Filter downloaded games", "filter": "Filter downloaded games",
"remove": "Remove", "remove": "Remove",
"downloading_metadata": "Downloading metadata…", "downloading_metadata": "Downloading metadata…",
@@ -442,15 +404,7 @@
"resume_seeding": "Resume seeding", "resume_seeding": "Resume seeding",
"options": "Manage", "options": "Manage",
"extract": "Extract files", "extract": "Extract files",
"extracting": "Extracting files…", "extracting": "Extracting files…"
"delete_archive_title": "Would you like to delete {{fileName}}?",
"delete_archive_description": "The file has been successfully extracted and it's no longer needed.",
"yes": "Yes",
"no": "No",
"network": "NETWORK",
"peak": "PEAK",
"move_up": "Move up",
"move_down": "Move down"
}, },
"settings": { "settings": {
"downloads_path": "Downloads path", "downloads_path": "Downloads path",
@@ -586,7 +540,6 @@
"show_download_speed_in_megabytes": "Show download speed in megabytes per second", "show_download_speed_in_megabytes": "Show download speed in megabytes per second",
"extract_files_by_default": "Extract files by default after download", "extract_files_by_default": "Extract files by default after download",
"enable_steam_achievements": "Enable search for Steam achievements", "enable_steam_achievements": "Enable search for Steam achievements",
"enable_new_download_options_badges": "Show new download options badges",
"achievement_custom_notification_position": "Achievement custom notification position", "achievement_custom_notification_position": "Achievement custom notification position",
"top-left": "Top left", "top-left": "Top left",
"top-center": "Top center", "top-center": "Top center",
@@ -602,22 +555,10 @@
"platinum": "Platinum", "platinum": "Platinum",
"hidden": "Hidden", "hidden": "Hidden",
"test_notification": "Test notification", "test_notification": "Test notification",
"achievement_sound_volume": "Achievement sound volume",
"select_achievement_sound": "Select achievement sound",
"change_achievement_sound": "Change achievement sound",
"remove_achievement_sound": "Remove achievement sound",
"preview_sound": "Preview sound",
"select": "Select",
"preview": "Preview",
"remove": "Remove",
"no_sound_file_selected": "No sound file selected",
"notification_preview": "Achievement Notification Preview", "notification_preview": "Achievement Notification Preview",
"enable_friend_start_game_notifications": "When a friend starts playing a game", "enable_friend_start_game_notifications": "When a friend starts playing a game",
"autoplay_trailers_on_game_page": "Automatically start playing trailers on game page", "autoplay_trailers_on_game_page": "Automatically start playing trailers on game page",
"hide_to_tray_on_game_start": "Hide Hydra to tray on game startup", "hide_to_tray_on_game_start": "Hide Hydra to tray on game startup"
"downloads": "Downloads",
"use_native_http_downloader": "Use native HTTP downloader (experimental)",
"cannot_change_downloader_while_downloading": "Cannot change this setting while a download is in progress"
}, },
"notifications": { "notifications": {
"download_complete": "Download complete", "download_complete": "Download complete",
@@ -635,11 +576,7 @@
"game_extracted": "{{title}} extracted successfully", "game_extracted": "{{title}} extracted successfully",
"friend_started_playing_game": "{{displayName}} started playing a game", "friend_started_playing_game": "{{displayName}} started playing a game",
"test_achievement_notification_title": "This is a test notification", "test_achievement_notification_title": "This is a test notification",
"test_achievement_notification_description": "Pretty cool, huh?", "test_achievement_notification_description": "Pretty cool, huh?"
"scan_games_complete_title": "Scanning for games finished successfully",
"scan_games_complete_description": "Found {{count}} games without executable path set",
"scan_games_no_results_title": "Scanning for games finished",
"scan_games_no_results_description": "No installed games were found"
}, },
"system_tray": { "system_tray": {
"open": "Open Hydra", "open": "Open Hydra",
@@ -698,7 +635,6 @@
"sending": "Sending", "sending": "Sending",
"friend_request_sent": "Friend request sent", "friend_request_sent": "Friend request sent",
"friends": "Friends", "friends": "Friends",
"badges": "Badges",
"friends_list": "Friends list", "friends_list": "Friends list",
"user_not_found": "User not found", "user_not_found": "User not found",
"block_user": "Block user", "block_user": "Block user",
@@ -709,17 +645,12 @@
"ignore_request": "Ignore request", "ignore_request": "Ignore request",
"cancel_request": "Cancel request", "cancel_request": "Cancel request",
"undo_friendship": "Undo friendship", "undo_friendship": "Undo friendship",
"friendship_removed": "Friend removed",
"request_accepted": "Request accepted", "request_accepted": "Request accepted",
"user_blocked_successfully": "User blocked successfully", "user_blocked_successfully": "User blocked successfully",
"user_block_modal_text": "This will block {{displayName}}", "user_block_modal_text": "This will block {{displayName}}",
"blocked_users": "Blocked users", "blocked_users": "Blocked users",
"unblock": "Unblock", "unblock": "Unblock",
"no_friends_added": "You have no added friends", "no_friends_added": "You have no added friends",
"no_friends_yet": "You haven't added any friends yet",
"view_all": "View all",
"load_more": "Load more",
"loading": "Loading",
"pending": "Pending", "pending": "Pending",
"no_pending_invites": "You have no pending invites", "no_pending_invites": "You have no pending invites",
"no_blocked_users": "You have no blocked users", "no_blocked_users": "You have no blocked users",
@@ -743,17 +674,21 @@
"report_reason_other": "Other", "report_reason_other": "Other",
"profile_reported": "Profile reported", "profile_reported": "Profile reported",
"your_friend_code": "Your friend code:", "your_friend_code": "Your friend code:",
"copy_friend_code": "Copy friend code",
"copied": "Copied!",
"upload_banner": "Upload banner", "upload_banner": "Upload banner",
"uploading_banner": "Uploading banner…", "uploading_banner": "Uploading banner…",
"change_banner": "Change banner",
"replace_banner": "Replace banner",
"remove_banner": "Remove banner",
"remove_banner_modal_title": "Remove banner?",
"remove_banner_confirmation": "Are you sure you want to remove your banner? You can always pick a new one when you want.",
"remove": "Remove",
"background_image_updated": "Background image updated", "background_image_updated": "Background image updated",
"crop_profile_image": "Crop Profile Image",
"crop_background_image": "Crop Background Image",
"crop": "Crop",
"cropping": "Cropping…",
"crop_area": "Crop area",
"resize_handle_nw": "Resize handle - northwest corner",
"resize_handle_ne": "Resize handle - northeast corner",
"resize_handle_sw": "Resize handle - southwest corner",
"resize_handle_se": "Resize handle - southeast corner",
"zoom_in": "Zoom In",
"zoom_out": "Zoom Out",
"image_crop_failure": "Failed to crop image. Please try again.",
"stats": "Stats", "stats": "Stats",
"achievements": "achievements", "achievements": "achievements",
"games": "Games", "games": "Games",
@@ -770,31 +705,10 @@
"game_added_to_pinned": "Game added to pinned", "game_added_to_pinned": "Game added to pinned",
"karma": "Karma", "karma": "Karma",
"karma_count": "karma", "karma_count": "karma",
"karma_description": "Earned from positive likes on reviews",
"user_reviews": "Reviews", "user_reviews": "Reviews",
"delete_review": "Delete Review", "delete_review": "Delete Review",
"loading_reviews": "Loading reviews...", "loading_reviews": "Loading reviews..."
"wrapped_2025": "Wrapped 2025"
},
"library": {
"library": "Library",
"play": "Play",
"download": "Download",
"downloading": "Downloading",
"game": "game",
"games": "games",
"grid_view": "Grid view",
"compact_view": "Compact view",
"large_view": "Large view",
"no_games_title": "Your library is empty",
"no_games_description": "Add games from the catalogue or download them to get started",
"amount_hours": "{{amount}} hours",
"amount_minutes": "{{amount}} minutes",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "This playtime has been manually updated",
"all_games": "All Games",
"recently_played": "Recently Played",
"favorites": "Favorites"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Achievement unlocked", "achievement_unlocked": "Achievement unlocked",
@@ -824,41 +738,5 @@
"hydra_cloud_feature_found": "You've just discovered a Hydra Cloud feature!", "hydra_cloud_feature_found": "You've just discovered a Hydra Cloud feature!",
"learn_more": "Learn More", "learn_more": "Learn More",
"debrid_description": "Download up to 4x faster with Nimbus" "debrid_description": "Download up to 4x faster with Nimbus"
},
"notifications_page": {
"title": "Notifications",
"mark_all_as_read": "Mark all as read",
"clear_all": "Clear All",
"loading": "Loading...",
"empty_title": "No notifications",
"empty_description": "You're all caught up! Check back later for new updates.",
"empty_filter_description": "No notifications match this filter.",
"filter_all": "All",
"filter_unread": "Unread",
"filter_friends": "Friends",
"filter_badges": "Badges",
"filter_upvotes": "Upvotes",
"filter_local": "Local",
"load_more": "Load more",
"dismiss": "Dismiss",
"accept": "Accept",
"refuse": "Refuse",
"notification": "Notification",
"friend_request_received_title": "New friend request!",
"friend_request_received_description": "{{displayName}} wants to be your friend",
"friend_request_accepted_title": "Friend request accepted!",
"friend_request_accepted_description": "{{displayName}} accepted your friend request",
"badge_received_title": "You got a new badge!",
"badge_received_description": "{{badgeName}}",
"review_upvote_title": "Your review for {{gameTitle}} got upvotes!",
"review_upvote_description": "Your review received {{count}} new upvotes",
"marked_all_as_read": "All notifications marked as read",
"failed_to_mark_as_read": "Failed to mark notifications as read",
"cleared_all": "All notifications cleared",
"failed_to_clear": "Failed to clear notifications",
"failed_to_load": "Failed to load notifications",
"failed_to_dismiss": "Failed to dismiss notification",
"friend_request_accepted": "Friend request accepted",
"friend_request_refused": "Friend request refused"
} }
} }

View File

@@ -13,7 +13,6 @@
}, },
"sidebar": { "sidebar": {
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Librería",
"downloads": "Descargas", "downloads": "Descargas",
"settings": "Ajustes", "settings": "Ajustes",
"my_library": "Mi Librería", "my_library": "Mi Librería",
@@ -26,7 +25,6 @@
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado", "game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
"sign_in": "Iniciar Sesión", "sign_in": "Iniciar Sesión",
"friends": "Amigos", "friends": "Amigos",
"notifications": "Notificaciones",
"need_help": "¿Necesitás ayuda?", "need_help": "¿Necesitás ayuda?",
"favorites": "Favoritos", "favorites": "Favoritos",
"playable_button_title": "Solo mostrar juegos que podés jugar en este momento", "playable_button_title": "Solo mostrar juegos que podés jugar en este momento",
@@ -94,16 +92,8 @@
}, },
"header": { "header": {
"search": "Buscar juegos", "search": "Buscar juegos",
"search_library": "Buscar en la librería",
"recent_searches": "Búsquedas Recientes",
"suggestions": "Sugerencias",
"clear_history": "Limpiar",
"remove_from_history": "Eliminar del historial",
"loading": "Cargando...",
"no_results": "Sin resultados",
"home": "Inicio", "home": "Inicio",
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Librería",
"downloads": "Descargas", "downloads": "Descargas",
"search_results": "Resultados de búsqueda", "search_results": "Resultados de búsqueda",
"settings": "Ajustes", "settings": "Ajustes",
@@ -116,7 +106,6 @@
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Restante {{eta}} - {{speed}}", "downloading": "Descargando {{title}}… ({{percentage}} completado) - Restante {{eta}} - {{speed}}",
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Comprobando tiempo restante…", "calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Comprobando tiempo restante…",
"checking_files": "Revisando archivos de {{title}}… ({{percentage}} completado)", "checking_files": "Revisando archivos de {{title}}… ({{percentage}} completado)",
"extracting": "Extrayendo {{title}}… ({{percentage}} completado)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Instalación completada", "installation_complete": "Instalación completada",
"installation_complete_message": "Common redistributables instalados correctamente" "installation_complete_message": "Common redistributables instalados correctamente"
@@ -175,7 +164,6 @@
"repacks_modal_description": "Elegí el repack que querés descargar", "repacks_modal_description": "Elegí el repack que querés descargar",
"select_folder_hint": "Si querés cambiar la carpeta por defecto, andá a <0>Ajustes</0>", "select_folder_hint": "Si querés cambiar la carpeta por defecto, andá a <0>Ajustes</0>",
"download_now": "Descargar ahora", "download_now": "Descargar ahora",
"loading": "Cargando...",
"no_shop_details": "No se pudieron obtener detalles de la tienda.", "no_shop_details": "No se pudieron obtener detalles de la tienda.",
"download_options": "Opciones de descarga", "download_options": "Opciones de descarga",
"download_path": "Ruta de descarga", "download_path": "Ruta de descarga",
@@ -185,12 +173,6 @@
"open_screenshot": "Abrir captura número {{number}}", "open_screenshot": "Abrir captura número {{number}}",
"download_settings": "Descargar ajustes", "download_settings": "Descargar ajustes",
"downloader": "Descargador", "downloader": "Descargador",
"downloader_online": "En línea",
"downloader_not_configured": "Disponible pero no configurado",
"downloader_offline": "El enlace está fuera de línea",
"downloader_not_available": "No disponible",
"recommended": "Recomendado",
"go_to_settings": "Ir a Ajustes",
"select_executable": "Seleccionar", "select_executable": "Seleccionar",
"no_executable_selected": "Sin ejecutable seleccionado", "no_executable_selected": "Sin ejecutable seleccionado",
"open_folder": "Abrir carpeta", "open_folder": "Abrir carpeta",
@@ -209,9 +191,7 @@
"danger_zone_section_description": "Remover este juego de tu librería o los archivos descargados por Hydra", "danger_zone_section_description": "Remover este juego de tu librería o los archivos descargados por Hydra",
"download_in_progress": "Descarga en progreso", "download_in_progress": "Descarga en progreso",
"download_paused": "Descarga pausada", "download_paused": "Descarga pausada",
"extracting": "Extrayendo",
"last_downloaded_option": "Última opción de descarga", "last_downloaded_option": "Última opción de descarga",
"new_download_option": "Nuevo",
"create_steam_shortcut": "Crear atajo de Steam", "create_steam_shortcut": "Crear atajo de Steam",
"create_shortcut_success": "Atajo creado con éxito", "create_shortcut_success": "Atajo creado con éxito",
"you_might_need_to_restart_steam": "Probablemente necesités reiniciar Steam para ver cambios", "you_might_need_to_restart_steam": "Probablemente necesités reiniciar Steam para ver cambios",
@@ -404,10 +384,6 @@
"completed": "Completado", "completed": "Completado",
"removed": "No descargado", "removed": "No descargado",
"cancel": "Cancelar", "cancel": "Cancelar",
"cancel_download": "¿Cancelar descarga?",
"cancel_download_description": "¿Estás seguro de que querés cancelar esta descarga? Todos los archivos descargados serán eliminados.",
"keep_downloading": "No, seguir descargando",
"yes_cancel": "Sí, cancelar",
"filter": "Filtrar juegos descargados", "filter": "Filtrar juegos descargados",
"remove": "Remover", "remove": "Remover",
"downloading_metadata": "Descargando metadatos…", "downloading_metadata": "Descargando metadatos…",
@@ -428,13 +404,7 @@
"resume_seeding": "Continuar sembrando", "resume_seeding": "Continuar sembrando",
"options": "Administrar", "options": "Administrar",
"extract": "Extraer archivos", "extract": "Extraer archivos",
"extracting": "Extrayendo archivos…", "extracting": "Extrayendo archivos…"
"delete_archive_title": "¿Querés eliminar {{fileName}}?",
"delete_archive_description": "El archivo se extrajo exitosamente y ya no es necesario.",
"yes": "Sí",
"no": "No",
"network": "RED",
"peak": "PICO"
}, },
"settings": { "settings": {
"downloads_path": "Ruta de descarga", "downloads_path": "Ruta de descarga",
@@ -478,7 +448,6 @@
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
"button_delete_all_sources": "Eliminar todo", "button_delete_all_sources": "Eliminar todo",
"added_download_source": "Añadir fuente de descarga", "added_download_source": "Añadir fuente de descarga",
"adding": "Añadiendo…",
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas", "download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
"insert_valid_json_url": "Introducí una URL de json válida", "insert_valid_json_url": "Introducí una URL de json válida",
"found_download_option_zero": "Sin opciones de descargas encontrada", "found_download_option_zero": "Sin opciones de descargas encontrada",
@@ -558,7 +527,6 @@
"show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo", "show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo",
"extract_files_by_default": "Extraer archivos por defecto después de descargar", "extract_files_by_default": "Extraer archivos por defecto después de descargar",
"enable_steam_achievements": "Habilitar búsqueda de logros de Steam", "enable_steam_achievements": "Habilitar búsqueda de logros de Steam",
"enable_new_download_options_badges": "Mostrar badges de nuevas opciones de descarga",
"achievement_custom_notification_position": "Posición de notificación de logros", "achievement_custom_notification_position": "Posición de notificación de logros",
"top-left": "Superior Izquierda", "top-left": "Superior Izquierda",
"top-center": "Superior Centro", "top-center": "Superior Centro",
@@ -574,21 +542,12 @@
"platinum": "Platino", "platinum": "Platino",
"hidden": "Oculto", "hidden": "Oculto",
"test_notification": "Probar notificación", "test_notification": "Probar notificación",
"achievement_sound_volume": "Volumen del sonido de logro",
"select_achievement_sound": "Seleccionar sonido de logro",
"select": "Seleccionar",
"preview": "Vista previa",
"remove": "Remover",
"no_sound_file_selected": "No se seleccionó ningún archivo de sonido",
"notification_preview": "Probar notificación de logro", "notification_preview": "Probar notificación de logro",
"debrid": "Debrid", "debrid": "Debrid",
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.", "debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego", "enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego", "autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego", "hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
"downloads": "Descargas",
"use_native_http_downloader": "Usar descargador HTTP nativo (experimental)",
"cannot_change_downloader_while_downloading": "No se puede cambiar esta configuración mientras una descarga está en progreso"
}, },
"notifications": { "notifications": {
"download_complete": "Descarga completada", "download_complete": "Descarga completada",
@@ -662,7 +621,6 @@
"sending": "Enviando", "sending": "Enviando",
"friend_request_sent": "Solicitud de amistad enviada", "friend_request_sent": "Solicitud de amistad enviada",
"friends": "Amistades", "friends": "Amistades",
"badges": "Insignias",
"friends_list": "Lista de amistades", "friends_list": "Lista de amistades",
"user_not_found": "Usuario no encontrado", "user_not_found": "Usuario no encontrado",
"block_user": "Bloquear usuario", "block_user": "Bloquear usuario",
@@ -673,17 +631,12 @@
"ignore_request": "Ignorar solicitud", "ignore_request": "Ignorar solicitud",
"cancel_request": "Cancelar solicitud", "cancel_request": "Cancelar solicitud",
"undo_friendship": "Deshacer amistad", "undo_friendship": "Deshacer amistad",
"friendship_removed": "Amigo eliminado",
"request_accepted": "Solicitud aceptada", "request_accepted": "Solicitud aceptada",
"user_blocked_successfully": "Usuario bloqueado exitosamente", "user_blocked_successfully": "Usuario bloqueado exitosamente",
"user_block_modal_text": "Esto va a bloquear a {{displayName}}", "user_block_modal_text": "Esto va a bloquear a {{displayName}}",
"blocked_users": "Usuarios bloqueados", "blocked_users": "Usuarios bloqueados",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"no_friends_added": "No tenés amistades añadidas", "no_friends_added": "No tenés amistades añadidas",
"no_friends_yet": "Aún no has agregado ningún amigo",
"view_all": "Ver todo",
"load_more": "Cargar más",
"loading": "Cargando",
"pending": "Pendiente", "pending": "Pendiente",
"no_pending_invites": "No tenés invitaciones pendientes", "no_pending_invites": "No tenés invitaciones pendientes",
"no_blocked_users": "No has bloqueado a nadie", "no_blocked_users": "No has bloqueado a nadie",
@@ -707,16 +660,8 @@
"report_reason_other": "Otros", "report_reason_other": "Otros",
"profile_reported": "Perfil reportado", "profile_reported": "Perfil reportado",
"your_friend_code": "Tu código de amistad:", "your_friend_code": "Tu código de amistad:",
"copy_friend_code": "Copiar código de amistad",
"copied": "¡Copiado!",
"upload_banner": "Subir banner", "upload_banner": "Subir banner",
"uploading_banner": "Subiendo banner…", "uploading_banner": "Subiendo banner…",
"change_banner": "Cambiar banner",
"replace_banner": "Reemplazar banner",
"remove_banner": "Eliminar banner",
"remove_banner_modal_title": "¿Eliminar banner?",
"remove_banner_confirmation": "¿Estás seguro de que querés eliminar tu banner? Siempre podés elegir uno nuevo cuando quieras.",
"remove": "Eliminar",
"background_image_updated": "Imagen de fondo actualizada", "background_image_updated": "Imagen de fondo actualizada",
"stats": "Estadísticas", "stats": "Estadísticas",
"achievements": "logros", "achievements": "logros",
@@ -735,11 +680,11 @@
"amount_minutes_short": "{{amount}}m", "amount_minutes_short": "{{amount}}m",
"karma": "Karma", "karma": "Karma",
"karma_count": "karma", "karma_count": "karma",
"karma_description": "Conseguido por me gustas positivos en reseñas",
"sort_by": "Filtrar por:", "sort_by": "Filtrar por:",
"game_added_to_pinned": "Juego añadido a fijados", "game_added_to_pinned": "Juego añadido a fijados",
"user_reviews": "Reseñas", "user_reviews": "Reseñas",
"loading_reviews": "Cargando reseñas...", "loading_reviews": "Cargando reseñas...",
"wrapped_2025": "Wrapped 2025",
"no_reviews": "Sin reseñas aún", "no_reviews": "Sin reseñas aún",
"delete_review": "Eliminar reseña" "delete_review": "Eliminar reseña"
}, },
@@ -771,62 +716,5 @@
"hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!", "hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!",
"learn_more": "Descubrir más", "learn_more": "Descubrir más",
"debrid_description": "Descargas hasta x4 veces más rápidas con Nimbus" "debrid_description": "Descargas hasta x4 veces más rápidas con Nimbus"
},
"library": {
"library": "Librería",
"play": "Jugar",
"download": "Descargar",
"downloading": "Descargando",
"game": "juego",
"games": "juegos",
"grid_view": "Vista de cuadrícula",
"compact_view": "Vista compacta",
"large_view": "Vista grande",
"no_games_title": "Tu librería está vacía",
"no_games_description": "Agregá juegos del catálogo o descargalos para comenzar",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "Este tiempo de juego ha sido modificado manualmente",
"all_games": "Todos los Juegos",
"recently_played": "Jugados Recientemente",
"favorites": "Favoritos"
},
"notifications_page": {
"title": "Notificaciones",
"mark_all_as_read": "Marcar todo como leído",
"clear_all": "Limpiar todo",
"loading": "Cargando...",
"empty_title": "Sin notificaciones",
"empty_description": "¡Estás al día! Volvé más tarde para ver nuevas actualizaciones.",
"empty_filter_description": "No hay notificaciones que coincidan con este filtro.",
"filter_all": "Todas",
"filter_unread": "No leídas",
"filter_friends": "Amigos",
"filter_badges": "Insignias",
"filter_upvotes": "Votos",
"filter_local": "Locales",
"load_more": "Cargar más",
"dismiss": "Descartar",
"accept": "Aceptar",
"refuse": "Rechazar",
"notification": "Notificación",
"friend_request_received_title": "¡Nueva solicitud de amistad!",
"friend_request_received_description": "{{displayName}} quiere ser tu amigo",
"friend_request_accepted_title": "¡Solicitud de amistad aceptada!",
"friend_request_accepted_description": "{{displayName}} aceptó tu solicitud de amistad",
"badge_received_title": "¡Obtuviste una nueva insignia!",
"badge_received_description": "{{badgeName}}",
"review_upvote_title": "¡Tu reseña de {{gameTitle}} recibió votos!",
"review_upvote_description": "Tu reseña recibió {{count}} nuevos votos",
"marked_all_as_read": "Todas las notificaciones marcadas como leídas",
"failed_to_mark_as_read": "Error al marcar las notificaciones como leídas",
"cleared_all": "Todas las notificaciones eliminadas",
"failed_to_clear": "Error al eliminar las notificaciones",
"failed_to_load": "Error al cargar las notificaciones",
"failed_to_dismiss": "Error al descartar la notificación",
"friend_request_accepted": "Solicitud de amistad aceptada",
"friend_request_refused": "Solicitud de amistad rechazada"
} }
} }

View File

@@ -673,7 +673,8 @@
"game_removed_from_pinned": "Peli poistettu kiinnitetyistä", "game_removed_from_pinned": "Peli poistettu kiinnitetyistä",
"game_added_to_pinned": "Peli lisätty kiinnitettyihin", "game_added_to_pinned": "Peli lisätty kiinnitettyihin",
"karma": "Karma", "karma": "Karma",
"karma_count": "karmaa" "karma_count": "karmaa",
"karma_description": "Ansittu positiivisilla arvosteluäänillä"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Saavutus avattu", "achievement_unlocked": "Saavutus avattu",

View File

@@ -27,69 +27,7 @@
"friends": "Amis", "friends": "Amis",
"need_help": "Besoin d'aide ?", "need_help": "Besoin d'aide ?",
"favorites": "Favoris", "favorites": "Favoris",
"playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant", "playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant"
"library": "Bibliothèque",
"add_custom_game_tooltip": "Ajouter un jeu personnalisé",
"show_playable_only_tooltip": "Afficher uniquement les jeux jouables",
"custom_game_modal": "Ajouter un jeu personnalisé",
"custom_game_modal_description": "Ajoutez un jeu personnalisé à votre bibliothèque en sélectionnant un fichier exécutable",
"custom_game_modal_executable_path": "Chemin de l'exécutable",
"custom_game_modal_select_executable": "Sélectionner un fichier exécutable",
"custom_game_modal_title": "Titre",
"custom_game_modal_enter_title": "Entrer le titre",
"custom_game_modal_browse": "Parcourir",
"custom_game_modal_cancel": "Annuler",
"custom_game_modal_add": "Ajouter le jeu",
"custom_game_modal_adding": "Ajout du jeu…",
"custom_game_modal_success": "Jeu personnalisé ajouté avec succès",
"custom_game_modal_failed": "Échec de lajout du jeu personnalisé",
"custom_game_modal_executable": "Exécutable",
"edit_game_modal": "Personnaliser les ressources",
"edit_game_modal_description": "Personnalisez les ressources et les détails du jeu",
"edit_game_modal_title": "Titre",
"edit_game_modal_enter_title": "Entrer le titre",
"edit_game_modal_image": "Image",
"edit_game_modal_select_image": "Sélectionner une image",
"edit_game_modal_browse": "Parcourir",
"edit_game_modal_image_preview": "Aperçu de limage",
"edit_game_modal_icon": "Icône",
"edit_game_modal_select_icon": "Sélectionner une icône",
"edit_game_modal_icon_preview": "Aperçu de licône",
"edit_game_modal_logo": "Logo",
"edit_game_modal_select_logo": "Sélectionner un logo",
"edit_game_modal_logo_preview": "Aperçu du logo",
"edit_game_modal_hero": "Bannière de la bibliothèque",
"edit_game_modal_select_hero": "Sélectionner limage de bannière",
"edit_game_modal_hero_preview": "Aperçu de la bannière",
"edit_game_modal_cancel": "Annuler",
"edit_game_modal_update": "Mettre à jour",
"edit_game_modal_updating": "Mise à jour…",
"edit_game_modal_fill_required": "Veuillez remplir tous les champs requis",
"edit_game_modal_success": "Ressources mises à jour avec succès",
"edit_game_modal_failed": "Échec de la mise à jour des ressources",
"edit_game_modal_image_filter": "Image",
"edit_game_modal_icon_resolution": "Résolution recommandée : 256x256px",
"edit_game_modal_logo_resolution": "Résolution recommandée : 640x360px",
"edit_game_modal_hero_resolution": "Résolution recommandée : 1920x620px",
"edit_game_modal_assets": "Ressources",
"edit_game_modal_drop_icon_image_here": "Déposez limage de licône ici",
"edit_game_modal_drop_logo_image_here": "Déposez limage du logo ici",
"edit_game_modal_drop_hero_image_here": "Déposez limage de la bannière ici",
"edit_game_modal_drop_to_replace_icon": "Déposez pour remplacer licône",
"edit_game_modal_drop_to_replace_logo": "Déposez pour remplacer le logo",
"edit_game_modal_drop_to_replace_hero": "Déposez pour remplacer la bannière",
"install_decky_plugin": "Installer le plugin Decky",
"update_decky_plugin": "Mettre à jour le plugin Decky",
"decky_plugin_installed_version": "Plugin Decky (v{{version}})",
"install_decky_plugin_title": "Installer le plugin Decky Hydra",
"install_decky_plugin_message": "Cela téléchargera et installera le plugin Hydra pour Decky Loader. Des permissions élevées peuvent être requises. Continuer ?",
"update_decky_plugin_title": "Mettre à jour le plugin Decky Hydra",
"update_decky_plugin_message": "Une nouvelle version du plugin Decky Hydra est disponible. Souhaitez-vous la mettre à jour maintenant ?",
"decky_plugin_installed": "Plugin Decky v{{version}} installé avec succès",
"decky_plugin_installation_failed": "Échec de linstallation du plugin Decky : {{error}}",
"decky_plugin_installation_error": "Erreur lors de linstallation du plugin Decky : {{error}}",
"confirm": "Confirmer",
"cancel": "Annuler"
}, },
"header": { "header": {
"search": "Rechercher", "search": "Rechercher",
@@ -99,15 +37,7 @@
"search_results": "Résultats de la recherche", "search_results": "Résultats de la recherche",
"settings": "Paramètres", "settings": "Paramètres",
"version_available_install": "Version {{version}} disponible. Cliquez ici pour redémarrer et installer.", "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.", "version_available_download": "Version {{version}} disponible. Cliquez ici pour télécharger."
"search_library": "Rechercher dans la bibliothèque",
"recent_searches": "Recherches récentes",
"suggestions": "Suggestions",
"clear_history": "Effacer",
"remove_from_history": "Supprimer de l'historique",
"loading": "Chargement…",
"no_results": "Aucun résultat",
"library": "Bibliothèque"
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "Aucun téléchargement en cours", "no_downloads_in_progress": "Aucun téléchargement en cours",
@@ -117,8 +47,7 @@
"checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)", "checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Installation terminée", "installation_complete": "Installation terminée",
"installation_complete_message": "Redistribuables communs installés avec succès", "installation_complete_message": "Redistribuables communs installés avec succès"
"extracting": "Extraction de {{title}}… ({{percentage}} terminé)"
}, },
"catalogue": { "catalogue": {
"search": "Filtrer…", "search": "Filtrer…",
@@ -269,113 +198,7 @@
"download_error_not_cached_on_hydra": "Ce téléchargement n'est pas disponible sur Nimbus.", "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_removed_from_favorites": "Jeu retiré des favoris",
"game_added_to_favorites": "Jeu ajouté aux favoris", "game_added_to_favorites": "Jeu ajouté aux favoris",
"automatically_extract_downloaded_files": "Extraire automatiquement les fichiers téléchargés", "automatically_extract_downloaded_files": "Extraire automatiquement les fichiers téléchargés"
"already_in_library": "Déjà dans la bibliothèque",
"create_shortcut_simple": "Créer un raccourci",
"properties": "Propriétés",
"extracting": "Extraction en cours",
"new_download_option": "Nouveau",
"create_steam_shortcut": "Créer un raccourci Steam",
"you_might_need_to_restart_steam": "Vous devrez peut-être redémarrer Steam pour voir les changements",
"add_to_favorites": "Ajouter aux favoris",
"remove_from_favorites": "Retirer des favoris",
"failed_update_favorites": "Échec de la mise à jour des favoris",
"game_removed_from_library": "Jeu retiré de la bibliothèque",
"failed_remove_from_library": "Échec de la suppression du jeu de la bibliothèque",
"files_removed_success": "Fichiers supprimés avec succès",
"failed_remove_files": "Échec de la suppression des fichiers",
"rating_count": "Évaluations",
"show_more": "Afficher plus",
"show_less": "Afficher moins",
"reviews": "Avis",
"review_played_for": "Temps de jeu",
"leave_a_review": "Laisser un avis",
"write_review_placeholder": "Partagez votre avis sur ce jeu…",
"sort_newest": "Les plus récents",
"sort_oldest": "Les plus anciens",
"sort_highest_score": "Meilleure note",
"sort_lowest_score": "Note la plus basse",
"sort_most_voted": "Les plus votés",
"no_reviews_yet": "Aucun avis pour le moment",
"be_first_to_review": "Soyez le premier à donner votre avis !",
"rating": "Note",
"rating_stats": "Évaluation",
"rating_very_negative": "Très négatif",
"rating_negative": "Négatif",
"rating_neutral": "Neutre",
"rating_positive": "Positif",
"rating_very_positive": "Très positif",
"submit_review": "Envoyer",
"submitting": "Envoi…",
"review_submitted_successfully": "Avis envoyé avec succès !",
"review_submission_failed": "Échec de lenvoi de lavis. Veuillez réessayer.",
"review_cannot_be_empty": "Le champ de lavis ne peut pas être vide.",
"review_deleted_successfully": "Avis supprimé avec succès.",
"review_deletion_failed": "Échec de la suppression de lavis.",
"loading_reviews": "Chargement des avis…",
"loading_more_reviews": "Chargement de plus davis…",
"load_more_reviews": "Charger plus davis",
"you_seemed_to_enjoy_this_game": "Vous semblez avoir apprécié ce jeu",
"would_you_recommend_this_game": "Souhaitez-vous laisser un avis sur ce jeu ?",
"yes": "Oui",
"maybe_later": "Peut-être plus tard",
"backup_failed": "Échec de la sauvegarde",
"update_playtime_title": "Mettre à jour le temps de jeu",
"update_playtime_description": "Mettre à jour manuellement le temps de jeu pour {{game}}",
"update_playtime": "Mettre à jour le temps de jeu",
"update_playtime_success": "Temps de jeu mis à jour avec succès",
"update_playtime_error": "Échec de la mise à jour du temps de jeu",
"update_game_playtime": "Mettre à jour le temps de jeu",
"manual_playtime_warning": "Vos heures seront marquées comme modifiées manuellement et cela ne peut pas être annulé.",
"manual_playtime_tooltip": "Ce temps de jeu a été modifié manuellement",
"game_removed_from_pinned": "Jeu retiré des épinglés",
"game_added_to_pinned": "Jeu ajouté aux épinglés",
"create_start_menu_shortcut": "Créer un raccourci dans le menu Démarrer",
"invalid_wine_prefix_path": "Chemin du préfixe Wine invalide",
"invalid_wine_prefix_path_description": "Le chemin du préfixe Wine est invalide. Veuillez vérifier et réessayer.",
"missing_wine_prefix": "Un préfixe Wine est requis pour créer une sauvegarde sous Linux",
"artifact_renamed": "Sauvegarde renommée avec succès",
"rename_artifact": "Renommer la sauvegarde",
"rename_artifact_description": "Renommez la sauvegarde avec un nom plus descriptif",
"artifact_name_label": "Nom de la sauvegarde",
"artifact_name_placeholder": "Entrez un nom pour la sauvegarde",
"save_changes": "Enregistrer les modifications",
"required_field": "Ce champ est requis",
"max_length_field": "Ce champ doit contenir moins de {{length}} caractères",
"freeze_backup": "Épingler pour éviter lécrasement automatique",
"unfreeze_backup": "Désépingler",
"backup_frozen": "Sauvegarde épinglée",
"backup_unfrozen": "Sauvegarde désépinglée",
"backup_freeze_failed": "Échec de lépinglage de la sauvegarde",
"backup_freeze_failed_description": "Vous devez laisser au moins un emplacement libre pour les sauvegardes automatiques",
"edit_game_modal_button": "Personnaliser les ressources du jeu",
"game_details": "Détails du jeu",
"prices": "Prix",
"no_prices_found": "Aucun prix trouvé",
"view_all_prices": "Cliquer pour voir tous les prix",
"retail_price": "Prix officiel",
"keyshop_price": "Prix Keyshop",
"historical_retail": "Historique officiel",
"historical_keyshop": "Historique Keyshop",
"language": "Langue",
"caption": "Sous-titres",
"audio": "Audio",
"filter_by_source": "Filtrer par source",
"no_repacks_found": "Aucune source trouvée pour ce jeu",
"delete_review": "Supprimer lavis",
"remove_review": "Retirer lavis",
"delete_review_modal_title": "Voulez-vous vraiment supprimer votre avis ?",
"delete_review_modal_description": "Cette action est irréversible.",
"delete_review_modal_delete_button": "Supprimer",
"delete_review_modal_cancel_button": "Annuler",
"vote_failed": "Échec de lenregistrement de votre vote. Veuillez réessayer.",
"show_original": "Afficher loriginal",
"show_translation": "Afficher la traduction",
"show_original_translated_from": "Afficher loriginal (traduit depuis {{language}})",
"hide_original": "Masquer loriginal",
"review_from_blocked_user": "Avis dun utilisateur bloqué",
"show": "Afficher",
"hide": "Masquer"
}, },
"activation": { "activation": {
"title": "Activer Hydra", "title": "Activer Hydra",
@@ -414,11 +237,7 @@
"resume_seeding": "Reprendre le partage", "resume_seeding": "Reprendre le partage",
"options": "Gérer", "options": "Gérer",
"extract": "Extraire les fichiers", "extract": "Extraire les fichiers",
"extracting": "Extraction des fichiers…", "extracting": "Extraction des fichiers…"
"delete_archive_title": "Voulez-vous supprimer {{fileName}} ?",
"delete_archive_description": "Le fichier a été extrait avec succès et nest plus nécessaire.",
"yes": "Oui",
"no": "Non"
}, },
"settings": { "settings": {
"downloads_path": "Chemin des téléchargements", "downloads_path": "Chemin des téléchargements",
@@ -547,40 +366,7 @@
"bottom-left": "En bas à gauche", "bottom-left": "En bas à gauche",
"bottom-center": "En bas au centre", "bottom-center": "En bas au centre",
"bottom-right": "En bas à droite", "bottom-right": "En bas à droite",
"enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu", "enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu"
"adding": "Ajout…",
"failed_add_download_source": "Échec de lajout de la source de téléchargement. Veuillez réessayer.",
"download_source_already_exists": "Cette URL de source existe déjà",
"download_source_pending_matching": "Mise à jour imminente",
"download_source_matched": "À jour",
"download_source_matching": "Mise à jour",
"download_source_failed": "Erreur",
"download_source_no_information": "Aucune information disponible",
"removed_all_download_sources": "Toutes les sources de téléchargement supprimées",
"download_sources_synced_successfully": "Toutes les sources de téléchargement ont été synchronisées",
"importing": "Importation…",
"hydra_cloud": "Hydra Cloud",
"debrid": "Debrid",
"enable_steam_achievements": "Activer la recherche de succès Steam",
"alignment": "Alignement",
"variation": "Variation",
"default": "Par défaut",
"rare": "Rare",
"platinum": "Platine",
"hidden": "Caché",
"test_notification": "Notification de test",
"achievement_sound_volume": "Volume du son de succès",
"select_achievement_sound": "Sélectionner un son de succès",
"change_achievement_sound": "Changer le son de succès",
"remove_achievement_sound": "Supprimer le son de succès",
"preview_sound": "Prévisualiser le son",
"select": "Sélectionner",
"preview": "Aperçu",
"remove": "Supprimer",
"no_sound_file_selected": "Aucun fichier sonore sélectionné",
"notification_preview": "Aperçu de la notification de succès",
"autoplay_trailers_on_game_page": "Lire automatiquement les bandes-annonces sur la page du jeu",
"hide_to_tray_on_game_start": "Réduire Hydra dans la barre système au lancement dun jeu"
}, },
"notifications": { "notifications": {
"download_complete": "Téléchargement terminé", "download_complete": "Téléchargement terminé",

View File

@@ -8,12 +8,11 @@
"no_results": "Nincs találat", "no_results": "Nincs találat",
"start_typing": "Kereséshez gépelj...", "start_typing": "Kereséshez gépelj...",
"hot": "Most felkapott", "hot": "Most felkapott",
"weekly": "📅 Heti kiemeltek", "weekly": "📅 A hét felkapottjai",
"achievements": "🏆 Achievement támogatott" "achievements": "🏆 Achievement támogatott"
}, },
"sidebar": { "sidebar": {
"catalogue": "Katalógus", "catalogue": "Katalógus",
"library": "Könyvtár",
"downloads": "Letöltések", "downloads": "Letöltések",
"settings": "Beállítások", "settings": "Beállítások",
"my_library": "Könyvtáram", "my_library": "Könyvtáram",
@@ -22,11 +21,10 @@
"downloading": "{{title}} ({{percentage}} - Letöltés…)", "downloading": "{{title}} ({{percentage}} - Letöltés…)",
"filter": "Könyvtár szűrése", "filter": "Könyvtár szűrése",
"home": "Főoldal", "home": "Főoldal",
"queued": "{{title}} (Várakozásban)", "queued": "A(z) {{title}} (Várakozósorban van)",
"game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl", "game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl",
"sign_in": "Bejelentkezés", "sign_in": "Bejelentkezés",
"friends": "Barátok", "friends": "Barátok",
"notifications": "Értesítések",
"need_help": "Elakadtál?", "need_help": "Elakadtál?",
"favorites": "Kedvenc Játékaim", "favorites": "Kedvenc Játékaim",
"playable_button_title": "Csak az azonnal játszható játékokat mutasd", "playable_button_title": "Csak az azonnal játszható játékokat mutasd",
@@ -83,7 +81,7 @@
"update_decky_plugin": "Decky Plugin Frissítése", "update_decky_plugin": "Decky Plugin Frissítése",
"decky_plugin_installed_version": "Decky Plugin (v{{version}})", "decky_plugin_installed_version": "Decky Plugin (v{{version}})",
"install_decky_plugin_title": "Telepítsd a Hydra Decky Plugint", "install_decky_plugin_title": "Telepítsd a Hydra Decky Plugint",
"install_decky_plugin_message": "Ez letölti és telepíti a Hydra plugint a Decky Loaderhez. Előfordulhat, hogy rendszergazdai jogosultságra lesz szükség. Folytatod?", "install_decky_plugin_message": "Ez letölti és telepíteni fogja a Hydra plugint a Decky Loaderhez. Előfordulhat, hogy rendszergazdai jogosultságra lesz szükség. Folytatod?",
"update_decky_plugin_title": "Hydra Decky Plugin Frissítése", "update_decky_plugin_title": "Hydra Decky Plugin Frissítése",
"update_decky_plugin_message": "Egy új verzió elérhető a Hydra Decky Pluginhoz. Szeretnéd frissíteni?", "update_decky_plugin_message": "Egy új verzió elérhető a Hydra Decky Pluginhoz. Szeretnéd frissíteni?",
"decky_plugin_installed": "Decky plugin v{{version}} sikeresen telepítve", "decky_plugin_installed": "Decky plugin v{{version}} sikeresen telepítve",
@@ -94,16 +92,8 @@
}, },
"header": { "header": {
"search": "Keresés", "search": "Keresés",
"search_library": "Könyvtár böngészése",
"recent_searches": "Korábbi Keresések",
"suggestions": "Találatok",
"clear_history": "Törlés",
"remove_from_history": "Törlés az előzményekből",
"loading": "Töltés...",
"no_results": "Nincs találat",
"home": "Főoldal", "home": "Főoldal",
"catalogue": "Katalógus", "catalogue": "Katalógus",
"library": "Könyvtár",
"downloads": "Letöltések", "downloads": "Letöltések",
"search_results": "Keresési találatok", "search_results": "Keresési találatok",
"settings": "Beállítások", "settings": "Beállítások",
@@ -116,7 +106,6 @@
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}", "downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}",
"calculating_eta": "{{title}} letöltése… ({{percentage}} kész) - Hátralévő idő…", "calculating_eta": "{{title}} letöltése… ({{percentage}} kész) - Hátralévő idő…",
"checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)", "checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)",
"extracting": "{{title}} kicsomagolása… ({{percentage}} kicsomagolva)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Telepítés befejezve", "installation_complete": "Telepítés befejezve",
"installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve" "installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve"
@@ -128,7 +117,7 @@
"tags": "Címkék", "tags": "Címkék",
"publishers": "Kiadók", "publishers": "Kiadók",
"download_sources": "Letöltési források", "download_sources": "Letöltési források",
"result_count": "{{resultCount}} találat", "result_count": "{{resultCount}} találatok",
"filter_count": "{{filterCount}} elérhető", "filter_count": "{{filterCount}} elérhető",
"clear_filters": "{{filterCount}} kiválaszott szűrő törlése" "clear_filters": "{{filterCount}} kiválaszott szűrő törlése"
}, },
@@ -173,24 +162,17 @@
"playing_now": "Játékban: ", "playing_now": "Játékban: ",
"change": "Változtatás", "change": "Változtatás",
"repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni", "repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni",
"select_folder_hint": "A letöltési mappát a <0>Beállításokban</0> változtathatod meg", "select_folder_hint": "A letöltési mappát a <0>Beállítások</0> menüjében változtathatod meg",
"download_now": "Letöltés", "download_now": "Letöltés",
"loading": "Töltés...",
"no_shop_details": "A bolt adatai nem érhetőek el.", "no_shop_details": "A bolt adatai nem érhetőek el.",
"download_options": "Letöltési opciók", "download_options": "Letöltési opciók",
"download_path": "Letöltési hely", "download_path": "Letöltis hely",
"previous_screenshot": "Előző screenshot", "previous_screenshot": "Előző screenshot",
"next_screenshot": "Következő screenshot", "next_screenshot": "Következő screenshot",
"screenshot": "Screenshot {{number}}", "screenshot": "Screenshot {{number}}",
"open_screenshot": "{{number}} Screenshot megnyitása ", "open_screenshot": "Screenshot megnyitása {{number}}",
"download_settings": "Letöltési beállítások", "download_settings": "Letöltési beállítások",
"downloader": "Letöltő", "downloader": "Letöltési mód",
"downloader_online": "Elérhető",
"downloader_not_configured": "Elérhető de nincs beállítva",
"downloader_offline": "A link nem elérhető",
"downloader_not_available": "Nem elérhető",
"recommended": "Ajánlott",
"go_to_settings": "Beállítások megnyitása",
"select_executable": "Tallózás", "select_executable": "Tallózás",
"no_executable_selected": "Nincs futtatható fájl tallózva", "no_executable_selected": "Nincs futtatható fájl tallózva",
"open_folder": "Mappa megnyitása", "open_folder": "Mappa megnyitása",
@@ -211,9 +193,7 @@
"danger_zone_section_description": "Itt eltávolítható a játék a könyvtáradból, vagy a fájlok amelyek a Hydra által lettek letöltve", "danger_zone_section_description": "Itt eltávolítható a játék a könyvtáradból, vagy a fájlok amelyek a Hydra által lettek letöltve",
"download_in_progress": "Letöltés folyamatban", "download_in_progress": "Letöltés folyamatban",
"download_paused": "Letöltés szüneteltetve", "download_paused": "Letöltés szüneteltetve",
"extracting": "Kicsomagolás",
"last_downloaded_option": "Utoljára letöltött", "last_downloaded_option": "Utoljára letöltött",
"new_download_option": "Új",
"create_steam_shortcut": "Steam parancsikon létrehozása", "create_steam_shortcut": "Steam parancsikon létrehozása",
"create_shortcut_success": "A parancsikon létrehozása sikeres", "create_shortcut_success": "A parancsikon létrehozása sikeres",
"you_might_need_to_restart_steam": "Lehetséges hogy újrakell indítsd a Steamet hogy lásd a változást.", "you_might_need_to_restart_steam": "Lehetséges hogy újrakell indítsd a Steamet hogy lásd a változást.",
@@ -243,7 +223,6 @@
"show_more": "Mutass többet", "show_more": "Mutass többet",
"show_less": "Mutass kevesebbet", "show_less": "Mutass kevesebbet",
"reviews": "Vélemények", "reviews": "Vélemények",
"review_played_for": "Játszva",
"leave_a_review": "Hagyd itt a véleményed", "leave_a_review": "Hagyd itt a véleményed",
"write_review_placeholder": "Oszd meg gondolatod a játékról...", "write_review_placeholder": "Oszd meg gondolatod a játékról...",
"sort_newest": "Legújabb", "sort_newest": "Legújabb",
@@ -382,10 +361,7 @@
"show_original": "Eredeti megjelenítése", "show_original": "Eredeti megjelenítése",
"show_translation": "Fordítás megjelenítése", "show_translation": "Fordítás megjelenítése",
"show_original_translated_from": "Eredeti megjelenítése (fordítva: {{language}})", "show_original_translated_from": "Eredeti megjelenítése (fordítva: {{language}})",
"hide_original": "Eredeti elrejtése", "hide_original": "Eredeti elrejtése"
"review_from_blocked_user": "Letiltott felhasználó véleménye",
"show": "Megjelenítés",
"hide": "Elrejtés"
}, },
"activation": { "activation": {
"title": "Hydra Aktiválása", "title": "Hydra Aktiválása",
@@ -413,7 +389,7 @@
"delete_modal_description": "Ez eltávolítja a telepítési fájlokat a számítógépedről", "delete_modal_description": "Ez eltávolítja a telepítési fájlokat a számítógépedről",
"install": "Telepít", "install": "Telepít",
"download_in_progress": "Folyamatban lévő", "download_in_progress": "Folyamatban lévő",
"queued_downloads": "Várakozásban lévő letöltések", "queued_downloads": "Várakozósoron lévő letöltések",
"downloads_completed": "Befejezett", "downloads_completed": "Befejezett",
"queued": "Várakozásban", "queued": "Várakozásban",
"no_downloads_title": "Oly üres..", "no_downloads_title": "Oly üres..",
@@ -424,13 +400,7 @@
"resume_seeding": "Seedelés folytatása", "resume_seeding": "Seedelés folytatása",
"options": "Kezelés", "options": "Kezelés",
"extract": "Fájlok kibontása", "extract": "Fájlok kibontása",
"extracting": "Fájlok kibontása…", "extracting": "Fájlok kibontása…"
"delete_archive_title": "Szeretnéd törölni ezt a fájlt? {{fileName}}",
"delete_archive_description": "A tömörített fájl ki lett csomagolva és többé nincs rá szükség.",
"yes": "Igen",
"no": "Nem",
"network": "HÁLÓZAT",
"peak": "CSÚCS"
}, },
"settings": { "settings": {
"downloads_path": "Letöltési útvonalak", "downloads_path": "Letöltési útvonalak",
@@ -454,7 +424,7 @@
"debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ", "debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ",
"save_changes": "Változtatások mentése", "save_changes": "Változtatások mentése",
"changes_saved": "Változtatások sikeresen mentve", "changes_saved": "Változtatások sikeresen mentve",
"download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból, ennek az URL Forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.", "download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL Forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
"validate_download_source": "Érvényesítés", "validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás", "remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása", "add_download_source": "Forrás hozáadása",
@@ -518,11 +488,11 @@
"no_email_account": "Még nincs beállított emailed", "no_email_account": "Még nincs beállított emailed",
"account_data_updated_successfully": "Fiókadatok változtatása sikeres", "account_data_updated_successfully": "Fiókadatok változtatása sikeres",
"renew_subscription": "Hydra Cloud Megújítása", "renew_subscription": "Hydra Cloud Megújítása",
"subscription_expired_at": "Az előfizetésed lejárt: {{date}}", "subscription_expired_at": "Az előfizetésed lejárt, ekkor: {{date}}",
"no_subscription": "Élvezd a Hydrát a lehető legjobb módon", "no_subscription": "Élvezd a Hydrát a lehető legjobb módon",
"become_subscriber": "Légy Hydra Cloud tag", "become_subscriber": "Légy Hydra Cloud tag",
"subscription_renew_cancelled": "Automatikus megújítás kikapcsolva", "subscription_renew_cancelled": "Automatikus megújítás kikapcsolva",
"subscription_renews_on": "Az előfizetésed megújul: {{date}}", "subscription_renews_on": "Az előfizetésed megújul, ekkor: {{date}}",
"bill_sent_until": "A következő számlát ezen napon küldjük", "bill_sent_until": "A következő számlát ezen napon küldjük",
"no_themes": "Úgy látszik nincs egyetlen témád sem még, de ne aggódj, kattints ide hogy elkészítsd a remekművedet.", "no_themes": "Úgy látszik nincs egyetlen témád sem még, de ne aggódj, kattints ide hogy elkészítsd a remekművedet.",
"editor_tab_code": "Code", "editor_tab_code": "Code",
@@ -566,7 +536,6 @@
"show_download_speed_in_megabytes": "Letöltési sebesség megabájt/másodpercben lévő megjelenítése", "show_download_speed_in_megabytes": "Letöltési sebesség megabájt/másodpercben lévő megjelenítése",
"extract_files_by_default": "Fájlok kicsomagolása letöltés után", "extract_files_by_default": "Fájlok kicsomagolása letöltés után",
"enable_steam_achievements": "Steam-achievementek utáni keresés engedélyezése", "enable_steam_achievements": "Steam-achievementek utáni keresés engedélyezése",
"enable_new_download_options_badges": "Új letöltési helyek",
"achievement_custom_notification_position": "Achievement-értesítések egyéni elhelyezése", "achievement_custom_notification_position": "Achievement-értesítések egyéni elhelyezése",
"top-left": "Bal felső sarok", "top-left": "Bal felső sarok",
"top-center": "Felső közép", "top-center": "Felső közép",
@@ -582,19 +551,10 @@
"platinum": "Platina", "platinum": "Platina",
"hidden": "Rejtett", "hidden": "Rejtett",
"test_notification": "Értesítés tesztelése", "test_notification": "Értesítés tesztelése",
"achievement_sound_volume": "Achievement hangereje",
"select_achievement_sound": "Achievement hang kiválasztása",
"change_achievement_sound": "Achievement hang megváltoztatása",
"remove_achievement_sound": "Achievement hang eltávolítása",
"preview_sound": "Hang előnézet",
"select": "Kiválaszt",
"preview": "Előnézet",
"remove": "Eltávolít",
"no_sound_file_selected": "Nincs hangfájl kiválasztva",
"notification_preview": "Achievement Értesítés Előnézete", "notification_preview": "Achievement Értesítés Előnézete",
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot", "enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot",
"autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán", "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
"hide_to_tray_on_game_start": "Hydra elrejtése játék indításakor a tálcára" "hide_to_tray_on_game_start": "Hydra elrejtése játék elindításakor a tálcára"
}, },
"notifications": { "notifications": {
"download_complete": "Letöltés befejezve", "download_complete": "Letöltés befejezve",
@@ -647,9 +607,9 @@
"sort_by": "Rendezés:", "sort_by": "Rendezés:",
"achievements_earned": "Elért achievementek", "achievements_earned": "Elért achievementek",
"played_recently": "Nemrég játszva", "played_recently": "Nemrég játszva",
"playtime": "Játékidő", "playtime": "Játszottidő",
"total_play_time": "Teljes játékidő", "total_play_time": "Teljes játszottidő",
"manual_playtime_tooltip": "Ez a játékidő manuálisan lett frissítve", "manual_playtime_tooltip": "Ez a játszottidő manuálisan lett frissítve",
"no_recent_activity_title": "Hmmm… itt semmi sincs", "no_recent_activity_title": "Hmmm… itt semmi sincs",
"no_recent_activity_description": "Mostanában nem játszottál semmivel. Hát ideje ezt megváltoztatni!", "no_recent_activity_description": "Mostanában nem játszottál semmivel. Hát ideje ezt megváltoztatni!",
"display_name": "Profilnév", "display_name": "Profilnév",
@@ -671,7 +631,6 @@
"sending": "Küldés..", "sending": "Küldés..",
"friend_request_sent": "Barátfelkérés elküldve", "friend_request_sent": "Barátfelkérés elküldve",
"friends": "Barátok", "friends": "Barátok",
"badges": "Kitűzők",
"friends_list": "Barát lista", "friends_list": "Barát lista",
"user_not_found": "Felhasználó nem találva", "user_not_found": "Felhasználó nem találva",
"block_user": "Felhasználó letiltása", "block_user": "Felhasználó letiltása",
@@ -682,22 +641,18 @@
"ignore_request": "Kérés ignorálása", "ignore_request": "Kérés ignorálása",
"cancel_request": "Kérés visszavonása", "cancel_request": "Kérés visszavonása",
"undo_friendship": "Barát eltávolítása", "undo_friendship": "Barát eltávolítása",
"friendship_removed": "Barát eltávolítva",
"request_accepted": "Barátfelkérés elfogadva", "request_accepted": "Barátfelkérés elfogadva",
"user_blocked_successfully": "Felhasználó sikeresen letiltva", "user_blocked_successfully": "Felhasználó sikeresen letiltva",
"user_block_modal_text": "Ez által letiltod őt: {{displayName}}", "user_block_modal_text": "Ez által letiltod őt: {{displayName}}",
"blocked_users": "Letiltott felhasználók", "blocked_users": "Letiltott felhasználók",
"unblock": "Tiltás feloldása", "unblock": "Tiltás feloldása",
"no_friends_added": "Nincs bejelölt barátod", "no_friends_added": "Nincs bejelölt barátod",
"view_all": "Összes megtekintése",
"load_more": "Több betöltése",
"loading": "Töltés..",
"pending": "Függőben", "pending": "Függőben",
"no_pending_invites": "Nincs függőben lévő barátfelkérésed", "no_pending_invites": "Nincs függőben lévő barátfelkérésed",
"no_blocked_users": "Nincs letiltott felhasználó", "no_blocked_users": "Nincs letiltott felhasználó",
"friend_code_copied": "Barát kód kimásolva", "friend_code_copied": "Barát kód kimásolva",
"undo_friendship_modal_text": "Ezáltal megszünteted a barátságod vele: {{displayName}}", "undo_friendship_modal_text": "Ezáltal megszünteted a barátságod vele: {{displayName}}",
"privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállításokba</0>", "privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállítások</0> menüjébe",
"locked_profile": "Ez a profil privát", "locked_profile": "Ez a profil privát",
"image_process_failure": "Hiba a kép feldolgozása közben", "image_process_failure": "Hiba a kép feldolgozása közben",
"required_field": "Ez a mező kötelező", "required_field": "Ez a mező kötelező",
@@ -715,8 +670,7 @@
"report_reason_other": "Egyéb", "report_reason_other": "Egyéb",
"profile_reported": "Profil bejelentve", "profile_reported": "Profil bejelentve",
"your_friend_code": "A barát kódod:", "your_friend_code": "A barát kódod:",
"copy_friend_code": "Barátkód kimásolása", "upload_banner": "Borítókép feltöltés",
"upload_banner": "Borítókép feltöltése",
"uploading_banner": "Borítókép feltöltése…", "uploading_banner": "Borítókép feltöltése…",
"background_image_updated": "Borítókép frissítve", "background_image_updated": "Borítókép frissítve",
"stats": "Statisztikák", "stats": "Statisztikák",
@@ -735,33 +689,7 @@
"game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez", "game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez",
"karma": "Karma", "karma": "Karma",
"karma_count": "karma", "karma_count": "karma",
"user_reviews": "Vélemények", "karma_description": "Pozitív értékelésekkel szerzett pontok"
"delete_review": "Vélemény Törlése",
"loading_reviews": "Vélemények betöltése...",
"wrapped_2025": "Wrapped 2025",
"view_my_wrapped_button": "Wrapped 2025 megtekintése",
"view_wrapped_button": "{{displayName}} Wrapped 2025 megtekintése"
},
"library": {
"library": "Könyvtár",
"play": "Játék",
"download": "Letöltés",
"downloading": "Letöltés..",
"game": "játék",
"games": "játékok",
"grid_view": "Rács nézet",
"compact_view": "Kompakt nézet",
"large_view": "Nagy nézet",
"no_games_title": "A könyvtárad üres",
"no_games_description": "Adj játékokat a katalógusból hozzá vagy töltsd le őket hogy bele vágj",
"amount_hours": "{{amount}} óra",
"amount_minutes": "{{amount}} perc",
"amount_hours_short": "{{amount}}ó",
"amount_minutes_short": "{{amount}}p",
"manual_playtime_tooltip": "Ez a játékidő manuálisan lett frissítve",
"all_games": "Összes Játék",
"recently_played": "Nemrég Játszva",
"favorites": "Kedvencek"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Achievement feloldva", "achievement_unlocked": "Achievement feloldva",
@@ -791,41 +719,5 @@
"hydra_cloud_feature_found": "Épp felfedeztél egy Hydra Cloud funkciót!", "hydra_cloud_feature_found": "Épp felfedeztél egy Hydra Cloud funkciót!",
"learn_more": "Tudj meg többet", "learn_more": "Tudj meg többet",
"debrid_description": "Akár 4x gyorsabb letöltés a Nimbusszal" "debrid_description": "Akár 4x gyorsabb letöltés a Nimbusszal"
},
"notifications_page": {
"title": "Értesítések",
"mark_all_as_read": "Megjelölés olvasottként",
"clear_all": "Összes Törlése",
"loading": "Töltés..",
"empty_title": "Nincsenek értesítések",
"empty_description": "Már mindet láttad! Nézz vissza később az újdonságokért.",
"empty_filter_description": "Nincs értesítés ami megfelel ennek a szűrőnek.",
"filter_all": "Összes",
"filter_unread": "Olvasatlan",
"filter_friends": "Barátok",
"filter_badges": "Kitűzők",
"filter_upvotes": "Felpontok",
"filter_local": "Helyi",
"load_more": "Több betöltése",
"dismiss": "Eltüntetés",
"accept": "Elfogad",
"refuse": "Elutasít",
"notification": "Értesítés",
"friend_request_received_title": "Új barátkérelem!",
"friend_request_received_description": "{{displayName}} a barátod szeretne lenni",
"friend_request_accepted_title": "Barátkérelem elfogadva!",
"friend_request_accepted_description": "{{displayName}} elfogadta a barátkérelmed",
"badge_received_title": "Kaptál egy új kitűzőt!",
"badge_received_description": "{{badgeName}}",
"review_upvote_title": "A véleményed a(z) {{gameTitle}} játékhoz felpont-ot kapott!",
"review_upvote_description": "A véleményed {{count}} új felpontot kapott",
"marked_all_as_read": "Összes értesítés olvasottnak jelölve",
"failed_to_mark_as_read": "Az értesítések olvasottnak jelölése nem sikerült",
"cleared_all": "Összes értesítés eltüntetve",
"failed_to_clear": "Az értesítések eltüntetése nem sikerült",
"failed_to_load": "Az értesítések betöltése nem sikerült",
"failed_to_dismiss": "Értesítés eltüntetése nem sikerült",
"friend_request_accepted": "Barátfelkérés elfogadva",
"friend_request_refused": "Barátfelkérés elutasítva"
} }
} }

View File

@@ -673,7 +673,8 @@
"game_removed_from_pinned": "Spēle dzēsta no piespraustajiem", "game_removed_from_pinned": "Spēle dzēsta no piespraustajiem",
"game_added_to_pinned": "Spēle pievienota piespraustajiem", "game_added_to_pinned": "Spēle pievienota piespraustajiem",
"karma": "Karma", "karma": "Karma",
"karma_count": "karma" "karma_count": "karma",
"karma_description": "Nopelnīta ar pozitīviem atsauksmju vērtējumiem"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Sasniegums atbloķēts", "achievement_unlocked": "Sasniegums atbloķēts",

View File

@@ -13,7 +13,6 @@
}, },
"sidebar": { "sidebar": {
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Biblioteca",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Ajustes", "settings": "Ajustes",
"my_library": "Biblioteca", "my_library": "Biblioteca",
@@ -26,7 +25,6 @@
"game_has_no_executable": "Jogo não possui executável selecionado", "game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login", "sign_in": "Login",
"friends": "Amigos", "friends": "Amigos",
"notifications": "Notificações",
"need_help": "Precisa de ajuda?", "need_help": "Precisa de ajuda?",
"favorites": "Favoritos", "favorites": "Favoritos",
"playable_button_title": "Mostrar apenas jogos que você pode jogar agora", "playable_button_title": "Mostrar apenas jogos que você pode jogar agora",
@@ -94,19 +92,11 @@
}, },
"header": { "header": {
"search": "Buscar jogos", "search": "Buscar jogos",
"search_library": "Buscar na biblioteca",
"recent_searches": "Buscas Recentes",
"suggestions": "Sugestões",
"clear_history": "Limpar",
"remove_from_history": "Remover do histórico",
"loading": "Carregando...",
"no_results": "Sem resultados",
"home": "Início",
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Biblioteca",
"downloads": "Downloads", "downloads": "Downloads",
"search_results": "Resultados da busca", "search_results": "Resultados da busca",
"settings": "Ajustes", "settings": "Ajustes",
"home": "Início",
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.", "version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download." "version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
}, },
@@ -116,7 +106,6 @@
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}", "downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…", "calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…",
"checking_files": "Verificando arquivos de {{title}}…", "checking_files": "Verificando arquivos de {{title}}…",
"extracting": "Extraindo {{title}}… ({{percentage}} concluído)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Instalação concluída", "installation_complete": "Instalação concluída",
"installation_complete_message": "Componentes recomendados instalados com sucesso" "installation_complete_message": "Componentes recomendados instalados com sucesso"
@@ -164,7 +153,6 @@
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar", "repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes</0>", "select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes</0>",
"download_now": "Iniciar download", "download_now": "Iniciar download",
"loading": "Carregando...",
"no_shop_details": "Não foi possível obter os detalhes da loja.", "no_shop_details": "Não foi possível obter os detalhes da loja.",
"download_options": "Opções de download", "download_options": "Opções de download",
"download_path": "Diretório de download", "download_path": "Diretório de download",
@@ -174,12 +162,6 @@
"open_screenshot": "Ver captura de tela {{number}}", "open_screenshot": "Ver captura de tela {{number}}",
"download_settings": "Ajustes do download", "download_settings": "Ajustes do download",
"downloader": "Downloader", "downloader": "Downloader",
"downloader_online": "Online",
"downloader_not_configured": "Disponível mas não configurado",
"downloader_offline": "Link está offline",
"downloader_not_available": "Não disponível",
"recommended": "Recomendado",
"go_to_settings": "Ir para Configurações",
"select_executable": "Explorar", "select_executable": "Explorar",
"no_executable_selected": "Nenhum executável selecionado", "no_executable_selected": "Nenhum executável selecionado",
"open_folder": "Abrir pasta", "open_folder": "Abrir pasta",
@@ -199,9 +181,7 @@
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra", "danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra",
"download_in_progress": "Download em andamento", "download_in_progress": "Download em andamento",
"download_paused": "Download pausado", "download_paused": "Download pausado",
"extracting": "Extraindo",
"last_downloaded_option": "Última opção baixada", "last_downloaded_option": "Última opção baixada",
"new_download_option": "Novo",
"create_steam_shortcut": "Criar atalho na Steam", "create_steam_shortcut": "Criar atalho na Steam",
"create_shortcut_success": "Atalho criado com sucesso", "create_shortcut_success": "Atalho criado com sucesso",
"you_might_need_to_restart_steam": "Você pode precisar reiniciar a Steam para ver as alterações", "you_might_need_to_restart_steam": "Você pode precisar reiniciar a Steam para ver as alterações",
@@ -370,7 +350,6 @@
"show_translation": "Mostrar tradução", "show_translation": "Mostrar tradução",
"show_original_translated_from": "Mostrar original (traduzido do {{language}})", "show_original_translated_from": "Mostrar original (traduzido do {{language}})",
"hide_original": "Ocultar original", "hide_original": "Ocultar original",
"vote_failed": "Falha ao registrar seu voto. Por favor, tente novamente.",
"rating_count": "Avaliação", "rating_count": "Avaliação",
"review_from_blocked_user": "Avaliação de usuário bloqueado", "review_from_blocked_user": "Avaliação de usuário bloqueado",
"show": "Mostrar", "show": "Mostrar",
@@ -393,10 +372,6 @@
"completed": "Concluído", "completed": "Concluído",
"removed": "Cancelado", "removed": "Cancelado",
"cancel": "Cancelar", "cancel": "Cancelar",
"cancel_download": "Cancelar download?",
"cancel_download_description": "Tem certeza de que deseja cancelar este download? Todos os arquivos baixados serão excluídos.",
"keep_downloading": "Não, continuar baixando",
"yes_cancel": "Sim, cancelar",
"filter": "Filtrar jogos baixados", "filter": "Filtrar jogos baixados",
"remove": "Remover", "remove": "Remover",
"downloading_metadata": "Baixando metadados…", "downloading_metadata": "Baixando metadados…",
@@ -417,13 +392,7 @@
"resume_seeding": "Semear", "resume_seeding": "Semear",
"options": "Gerenciar", "options": "Gerenciar",
"extract": "Extrair arquivos", "extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…", "extracting": "Extraindo arquivos…"
"delete_archive_title": "Deseja deletar {{fileName}}?",
"delete_archive_description": "O arquivo foi extraído com sucesso e não é mais necessário.",
"yes": "Sim",
"no": "Não",
"network": "REDE",
"peak": "PICO"
}, },
"settings": { "settings": {
"downloads_path": "Diretório dos downloads", "downloads_path": "Diretório dos downloads",
@@ -470,7 +439,6 @@
"download_sources_synced_successfully": "Fontes de download sincronizadas", "download_sources_synced_successfully": "Fontes de download sincronizadas",
"removed_download_source": "Fonte removida", "removed_download_source": "Fonte removida",
"removed_download_sources": "Fontes removidas", "removed_download_sources": "Fontes removidas",
"removed_all_download_sources": "Todas as fontes de download removidas",
"cancel_button_confirmation_delete_all_sources": "Não", "cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo", "confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo",
"title_confirmation_delete_all_sources": "Remover todas as fontes de download", "title_confirmation_delete_all_sources": "Remover todas as fontes de download",
@@ -496,7 +464,6 @@
"blocked_users": "Usuários bloqueados", "blocked_users": "Usuários bloqueados",
"user_unblocked": "Usuário desbloqueado", "user_unblocked": "Usuário desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada", "enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"hydra_cloud": "Hydra Cloud",
"launch_minimized": "Iniciar o Hydra minimizado", "launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado", "disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download", "seed_after_download_complete": "Semear após a conclusão do download",
@@ -559,7 +526,6 @@
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo", "show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
"extract_files_by_default": "Extrair arquivos automaticamente após o download", "extract_files_by_default": "Extrair arquivos automaticamente após o download",
"enable_steam_achievements": "Habilitar busca por conquistas da Steam", "enable_steam_achievements": "Habilitar busca por conquistas da Steam",
"enable_new_download_options_badges": "Mostrar badges de novas opções de download",
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas", "enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
"top-left": "Superior esquerdo", "top-left": "Superior esquerdo",
"top-center": "Superior central", "top-center": "Superior central",
@@ -575,22 +541,10 @@
"platinum": "Platina", "platinum": "Platina",
"hidden": "Oculta", "hidden": "Oculta",
"test_notification": "Testar notificação", "test_notification": "Testar notificação",
"achievement_sound_volume": "Volume do som de conquista",
"select_achievement_sound": "Selecionar som de conquista",
"change_achievement_sound": "Alterar som de conquista",
"remove_achievement_sound": "Remover som de conquista",
"preview_sound": "Reproduzir som",
"select": "Selecionar",
"preview": "Reproduzir",
"remove": "Remover",
"no_sound_file_selected": "Nenhum arquivo de som selecionado",
"notification_preview": "Prévia da Notificação de Conquistas", "notification_preview": "Prévia da Notificação de Conquistas",
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo", "enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
"autoplay_trailers_on_game_page": "Reproduzir trailers automaticamente na página do jogo", "autoplay_trailers_on_game_page": "Reproduzir trailers automaticamente na página do jogo",
"hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo", "hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo"
"downloads": "Downloads",
"use_native_http_downloader": "Usar downloader HTTP nativo (experimental)",
"cannot_change_downloader_while_downloading": "Não é possível alterar esta configuração enquanto um download estiver em andamento"
}, },
"notifications": { "notifications": {
"download_complete": "Download concluído", "download_complete": "Download concluído",
@@ -676,7 +630,6 @@
"see_profile": "Ver perfil", "see_profile": "Ver perfil",
"friend_request_sent": "Pedido de amizade enviado", "friend_request_sent": "Pedido de amizade enviado",
"friends": "Amigos", "friends": "Amigos",
"badges": "Insígnias",
"add": "Adicionar", "add": "Adicionar",
"sending": "Enviando", "sending": "Enviando",
"friends_list": "Lista de amigos", "friends_list": "Lista de amigos",
@@ -689,17 +642,12 @@
"ignore_request": "Ignorar pedido", "ignore_request": "Ignorar pedido",
"cancel_request": "Cancelar pedido", "cancel_request": "Cancelar pedido",
"undo_friendship": "Desfazer amizade", "undo_friendship": "Desfazer amizade",
"friendship_removed": "Amigo removido",
"request_accepted": "Pedido de amizade aceito", "request_accepted": "Pedido de amizade aceito",
"user_blocked_successfully": "Usuário bloqueado com sucesso", "user_blocked_successfully": "Usuário bloqueado com sucesso",
"user_block_modal_text": "Bloquear {{displayName}}", "user_block_modal_text": "Bloquear {{displayName}}",
"blocked_users": "Usuários bloqueados", "blocked_users": "Usuários bloqueados",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"no_friends_added": "Você ainda não possui amigos adicionados", "no_friends_added": "Você ainda não possui amigos adicionados",
"no_friends_yet": "Você ainda não adicionou nenhum amigo",
"view_all": "Ver todos",
"load_more": "Carregar mais",
"loading": "Carregando",
"pending": "Pendentes", "pending": "Pendentes",
"no_pending_invites": "Você não possui convites de amizade pendentes", "no_pending_invites": "Você não possui convites de amizade pendentes",
"no_blocked_users": "Você não tem nenhum usuário bloqueado", "no_blocked_users": "Você não tem nenhum usuário bloqueado",
@@ -723,16 +671,8 @@
"report_reason_other": "Outro", "report_reason_other": "Outro",
"profile_reported": "Perfil reportado", "profile_reported": "Perfil reportado",
"your_friend_code": "Seu código de amigo:", "your_friend_code": "Seu código de amigo:",
"copy_friend_code": "Copiar código de amigo",
"copied": "Copiado!",
"upload_banner": "Carregar banner", "upload_banner": "Carregar banner",
"uploading_banner": "Carregando banner…", "uploading_banner": "Carregando banner…",
"change_banner": "Alterar banner",
"replace_banner": "Substituir banner",
"remove_banner": "Remover banner",
"remove_banner_modal_title": "Remover banner?",
"remove_banner_confirmation": "Tem certeza de que deseja remover seu banner? Você sempre pode escolher um novo quando quiser.",
"remove": "Remover",
"background_image_updated": "Imagem de fundo salva", "background_image_updated": "Imagem de fundo salva",
"stats": "Estatísticas", "stats": "Estatísticas",
"achievements": "conquistas", "achievements": "conquistas",
@@ -756,10 +696,10 @@
"achievements_earned": "Conquistas recebidas", "achievements_earned": "Conquistas recebidas",
"karma": "Karma", "karma": "Karma",
"karma_count": "karma", "karma_count": "karma",
"karma_description": "Ganho a partir de curtidas positivas em avaliações",
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente", "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
"user_reviews": "Avaliações", "user_reviews": "Avaliações",
"loading_reviews": "Carregando avaliações...", "loading_reviews": "Carregando avaliações...",
"wrapped_2025": "Wrapped 2025",
"no_reviews": "Ainda não há avaliações", "no_reviews": "Ainda não há avaliações",
"delete_review": "Excluir avaliação" "delete_review": "Excluir avaliação"
}, },
@@ -791,62 +731,5 @@
"hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!", "hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!",
"learn_more": "Saiba mais", "learn_more": "Saiba mais",
"debrid_description": "Baixe até 4x mais rápido com Nimbus" "debrid_description": "Baixe até 4x mais rápido com Nimbus"
},
"library": {
"library": "Biblioteca",
"play": "Jogar",
"download": "Baixar",
"downloading": "Baixando",
"game": "jogo",
"games": "jogos",
"grid_view": "Visualização em grade",
"compact_view": "Visualização compacta",
"large_view": "Visualização grande",
"no_games_title": "Sua biblioteca está vazia",
"no_games_description": "Adicione jogos do catálogo ou baixe-os para começar",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
"all_games": "Todos os Jogos",
"recently_played": "Jogados Recentemente",
"favorites": "Favoritos"
},
"notifications_page": {
"title": "Notificações",
"mark_all_as_read": "Marcar todas como lidas",
"clear_all": "Limpar todas",
"loading": "Carregando...",
"empty_title": "Sem notificações",
"empty_description": "Você está em dia! Volte mais tarde para ver novas atualizações.",
"empty_filter_description": "Nenhuma notificação corresponde a este filtro.",
"filter_all": "Todas",
"filter_unread": "Não lidas",
"filter_friends": "Amigos",
"filter_badges": "Insígnias",
"filter_upvotes": "Votos",
"filter_local": "Locais",
"load_more": "Carregar mais",
"dismiss": "Descartar",
"accept": "Aceitar",
"refuse": "Recusar",
"notification": "Notificação",
"friend_request_received_title": "Nova solicitação de amizade!",
"friend_request_received_description": "{{displayName}} quer ser seu amigo",
"friend_request_accepted_title": "Solicitação de amizade aceita!",
"friend_request_accepted_description": "{{displayName}} aceitou sua solicitação de amizade",
"badge_received_title": "Você recebeu uma nova insígnia!",
"badge_received_description": "{{badgeName}}",
"review_upvote_title": "Sua avaliação de {{gameTitle}} recebeu votos!",
"review_upvote_description": "Sua avaliação recebeu {{count}} novos votos",
"marked_all_as_read": "Todas as notificações marcadas como lidas",
"failed_to_mark_as_read": "Falha ao marcar notificações como lidas",
"cleared_all": "Todas as notificações limpas",
"failed_to_clear": "Falha ao limpar notificações",
"failed_to_load": "Falha ao carregar notificações",
"failed_to_dismiss": "Falha ao descartar notificação",
"friend_request_accepted": "Solicitação de amizade aceita",
"friend_request_refused": "Solicitação de amizade recusada"
} }
} }

View File

@@ -30,19 +30,11 @@
}, },
"header": { "header": {
"search": "Procurar jogos", "search": "Procurar jogos",
"search_library": "Procurar na biblioteca",
"recent_searches": "Pesquisas Recentes",
"suggestions": "Sugestões",
"clear_history": "Limpar",
"remove_from_history": "Remover do histórico",
"loading": "A carregar...",
"no_results": "Sem resultados",
"home": "Início",
"catalogue": "Catálogo", "catalogue": "Catálogo",
"library": "Biblioteca",
"downloads": "Transferências", "downloads": "Transferências",
"search_results": "Resultados da pesquisa", "search_results": "Resultados da pesquisa",
"settings": "Definições", "settings": "Definições",
"home": "Início",
"version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.", "version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.",
"version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download." "version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download."
}, },
@@ -508,7 +500,7 @@
"show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores", "show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores",
"animated_profile_banner": "Banner animado no perfil", "animated_profile_banner": "Banner animado no perfil",
"cloud_saving": "Progresso dos jogos na nuvem", "cloud_saving": "Progresso dos jogos na nuvem",
"hydra_cloud_feature_found": "Descobriste uma funcionalidade Hydra Cloud!", "hydra_cloud_feature_found": "Descubriste uma funcionalidade Hydra Cloud!",
"learn_more": "Saber mais" "learn_more": "Saber mais"
} }
} }

View File

@@ -13,7 +13,6 @@
}, },
"sidebar": { "sidebar": {
"catalogue": "Каталог", "catalogue": "Каталог",
"library": "Библиотека",
"downloads": "Загрузки", "downloads": "Загрузки",
"settings": "Настройки", "settings": "Настройки",
"my_library": "Библиотека", "my_library": "Библиотека",
@@ -26,7 +25,6 @@
"game_has_no_executable": "Файл запуска игры не выбран", "game_has_no_executable": "Файл запуска игры не выбран",
"sign_in": "Войти", "sign_in": "Войти",
"friends": "Друзья", "friends": "Друзья",
"notifications": "Уведомления",
"need_help": "Нужна помощь?", "need_help": "Нужна помощь?",
"favorites": "Избранное", "favorites": "Избранное",
"playable_button_title": "Показать только установленные игры.", "playable_button_title": "Показать только установленные игры.",
@@ -94,16 +92,8 @@
}, },
"header": { "header": {
"search": "Поиск", "search": "Поиск",
"search_library": "Поиск в библиотеке",
"recent_searches": "Недавние поиски",
"suggestions": "Предложения",
"clear_history": "Очистить",
"remove_from_history": "Удалить из истории",
"loading": "Загрузка...",
"no_results": "Нет результатов",
"home": "Главная", "home": "Главная",
"catalogue": "Каталог", "catalogue": "Каталог",
"library": "Библиотека",
"downloads": "Загрузки", "downloads": "Загрузки",
"search_results": "Результаты поиска", "search_results": "Результаты поиска",
"settings": "Настройки", "settings": "Настройки",
@@ -116,7 +106,6 @@
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}", "downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}",
"calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…", "calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…",
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)", "checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
"extracting": "Распаковка {{title}}… ({{percentage}} завершено)",
"installing_common_redist": "{{log}}…", "installing_common_redist": "{{log}}…",
"installation_complete": "Установка завершена", "installation_complete": "Установка завершена",
"installation_complete_message": "Библиотеки успешно установлены" "installation_complete_message": "Библиотеки успешно установлены"
@@ -150,7 +139,7 @@
"filter": "Поиск репаков", "filter": "Поиск репаков",
"requirements": "Системные требования", "requirements": "Системные требования",
"minimum": "Минимальные", "minimum": "Минимальные",
"recommended": "Рекомендованные", "recommended": "Рекомендуемые",
"paused": "Приостановлено", "paused": "Приостановлено",
"release_date": "Выпущено {{date}}", "release_date": "Выпущено {{date}}",
"publisher": "Издатель {{publisher}}", "publisher": "Издатель {{publisher}}",
@@ -175,7 +164,6 @@
"repacks_modal_description": "Выберите репак для загрузки", "repacks_modal_description": "Выберите репак для загрузки",
"select_folder_hint": "Чтобы изменить папку загрузок по умолчанию, откройте <0>Настройки</0>", "select_folder_hint": "Чтобы изменить папку загрузок по умолчанию, откройте <0>Настройки</0>",
"download_now": "Загрузить сейчас", "download_now": "Загрузить сейчас",
"loading": "Загрузка...",
"no_shop_details": "Не удалось получить описание", "no_shop_details": "Не удалось получить описание",
"download_options": "Источники", "download_options": "Источники",
"download_path": "Путь для загрузок", "download_path": "Путь для загрузок",
@@ -185,11 +173,6 @@
"open_screenshot": "Открыть скриншот {{number}}", "open_screenshot": "Открыть скриншот {{number}}",
"download_settings": "Параметры загрузки", "download_settings": "Параметры загрузки",
"downloader": "Загрузчик", "downloader": "Загрузчик",
"downloader_online": "Онлайн",
"downloader_not_configured": "Доступен, но не настроен",
"downloader_offline": "Ссылка недоступна",
"downloader_not_available": "Недоступно",
"go_to_settings": "Перейти в настройки",
"select_executable": "Выбрать", "select_executable": "Выбрать",
"no_executable_selected": "Файл не выбран", "no_executable_selected": "Файл не выбран",
"open_folder": "Открыть папку", "open_folder": "Открыть папку",
@@ -210,9 +193,7 @@
"danger_zone_section_description": "Вы можете удалить эту игру из вашей библиотеки или файлы скачанные из Hydra", "danger_zone_section_description": "Вы можете удалить эту игру из вашей библиотеки или файлы скачанные из Hydra",
"download_in_progress": "Идёт загрузка", "download_in_progress": "Идёт загрузка",
"download_paused": "Загрузка приостановлена", "download_paused": "Загрузка приостановлена",
"extracting": "Распаковка",
"last_downloaded_option": "Последний вариант загрузки", "last_downloaded_option": "Последний вариант загрузки",
"new_download_option": "Новый",
"create_steam_shortcut": "Создать ярлык Steam", "create_steam_shortcut": "Создать ярлык Steam",
"create_shortcut_success": "Ярлык создан", "create_shortcut_success": "Ярлык создан",
"you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения", "you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения",
@@ -242,11 +223,11 @@
"show_more": "Показать больше", "show_more": "Показать больше",
"show_less": "Показать меньше", "show_less": "Показать меньше",
"reviews": "Отзывы", "reviews": "Отзывы",
"review_played_for": "Играли",
"leave_a_review": "Оставить отзыв", "leave_a_review": "Оставить отзыв",
"write_review_placeholder": "Поделитесь своими мыслями об этой игре...", "write_review_placeholder": "Поделитесь своими мыслями об этой игре...",
"sort_newest": "Сначала новые", "sort_newest": "Сначала новые",
"no_reviews_yet": "Пока нет отзывов", "no_reviews_yet": "Пока нет отзывов",
"review_played_for": "Играли",
"be_first_to_review": "Станьте первым, кто поделится своими мыслями об этой игре!", "be_first_to_review": "Станьте первым, кто поделится своими мыслями об этой игре!",
"sort_oldest": "Сначала старые", "sort_oldest": "Сначала старые",
"sort_highest_score": "Высший балл", "sort_highest_score": "Высший балл",
@@ -371,6 +352,8 @@
"audio": "Аудио", "audio": "Аудио",
"filter_by_source": "Фильтр по источнику", "filter_by_source": "Фильтр по источнику",
"no_repacks_found": "Источники для этой игры не найдены", "no_repacks_found": "Источники для этой игры не найдены",
"show": "Показать",
"hide": "Скрыть",
"delete_review": "Удалить отзыв", "delete_review": "Удалить отзыв",
"remove_review": "Удалить отзыв", "remove_review": "Удалить отзыв",
"delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?", "delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?",
@@ -382,9 +365,7 @@
"show_translation": "Показать перевод", "show_translation": "Показать перевод",
"show_original_translated_from": "Показать оригинал (переведено с {{language}})", "show_original_translated_from": "Показать оригинал (переведено с {{language}})",
"hide_original": "Скрыть оригинал", "hide_original": "Скрыть оригинал",
"review_from_blocked_user": "Отзыв от заблокированного пользователя", "review_from_blocked_user": "Отзыв от заблокированного пользователя"
"show": "Показать",
"hide": "Скрыть"
}, },
"activation": { "activation": {
"title": "Активировать Hydra", "title": "Активировать Hydra",
@@ -403,10 +384,6 @@
"completed": "Завершено", "completed": "Завершено",
"removed": "Не скачано", "removed": "Не скачано",
"cancel": "Отмена", "cancel": "Отмена",
"cancel_download": "Отменить загрузку?",
"cancel_download_description": "Вы уверены, что хотите отменить эту загрузку? Все загруженные файлы будут удалены.",
"keep_downloading": "Нет, продолжить загрузку",
"yes_cancel": "Да, отменить",
"filter": "Поиск загруженных игр", "filter": "Поиск загруженных игр",
"remove": "Удалить", "remove": "Удалить",
"downloading_metadata": "Загрузка метаданных…", "downloading_metadata": "Загрузка метаданных…",
@@ -427,13 +404,7 @@
"resume_seeding": "Продолжить раздачу", "resume_seeding": "Продолжить раздачу",
"options": "Управлять", "options": "Управлять",
"extract": "Распаковать файлы", "extract": "Распаковать файлы",
"extracting": "Распаковка файлов…", "extracting": "Распаковка файлов…"
"delete_archive_title": "Хотите удалить {{fileName}}?",
"delete_archive_description": "Файл был успешно распакован и больше не нужен.",
"yes": "Да",
"no": "Нет",
"network": "СЕТЬ",
"peak": "ПИК"
}, },
"settings": { "settings": {
"downloads_path": "Путь загрузок", "downloads_path": "Путь загрузок",
@@ -569,7 +540,6 @@
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду", "show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки", "extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
"enable_steam_achievements": "Включить поиск достижений Steam", "enable_steam_achievements": "Включить поиск достижений Steam",
"enable_new_download_options_badges": "Показывать значки новых вариантов загрузки",
"achievement_custom_notification_position": "Позиция уведомлений достижений", "achievement_custom_notification_position": "Позиция уведомлений достижений",
"top-left": "Верхний левый угол", "top-left": "Верхний левый угол",
"top-center": "Верхний центр", "top-center": "Верхний центр",
@@ -585,22 +555,10 @@
"platinum": "Платиновый", "platinum": "Платиновый",
"hidden": "Скрытый", "hidden": "Скрытый",
"test_notification": "Тестовое уведомление", "test_notification": "Тестовое уведомление",
"achievement_sound_volume": "Громкость звука достижения",
"select_achievement_sound": "Выбрать звук достижения",
"change_achievement_sound": "Изменить звук достижения",
"remove_achievement_sound": "Удалить звук достижения",
"preview_sound": "Предпросмотр звука",
"select": "Выбрать",
"preview": "Предпросмотр",
"remove": "Удалить",
"no_sound_file_selected": "Файл звука не выбран",
"notification_preview": "Предварительный просмотр уведомления о достижении", "notification_preview": "Предварительный просмотр уведомления о достижении",
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру", "enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
"autoplay_trailers_on_game_page": "Автоматически начинать воспроизведение трейлеров на странице игры", "autoplay_trailers_on_game_page": "Автоматически начинать воспроизведение трейлеров на странице игры",
"hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры", "hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры"
"downloads": "Загрузки",
"use_native_http_downloader": "Использовать встроенный HTTP-загрузчик (экспериментально)",
"cannot_change_downloader_while_downloading": "Нельзя изменить эту настройку во время загрузки"
}, },
"notifications": { "notifications": {
"download_complete": "Загрузка завершена", "download_complete": "Загрузка завершена",
@@ -677,7 +635,6 @@
"sending": "Отправка", "sending": "Отправка",
"friend_request_sent": "Запрос в друзья отправлен", "friend_request_sent": "Запрос в друзья отправлен",
"friends": "Друзья", "friends": "Друзья",
"badges": "Значки",
"friends_list": "Список друзей", "friends_list": "Список друзей",
"user_not_found": "Пользователь не найден", "user_not_found": "Пользователь не найден",
"block_user": "Заблокировать пользователя", "block_user": "Заблокировать пользователя",
@@ -688,17 +645,12 @@
"ignore_request": "Игнорировать запрос", "ignore_request": "Игнорировать запрос",
"cancel_request": "Отменить запрос", "cancel_request": "Отменить запрос",
"undo_friendship": "Удалить друга", "undo_friendship": "Удалить друга",
"friendship_removed": "Друг удален",
"request_accepted": "Запрос принят", "request_accepted": "Запрос принят",
"user_blocked_successfully": "Пользователь успешно заблокирован", "user_blocked_successfully": "Пользователь успешно заблокирован",
"user_block_modal_text": "{{displayName}} будет заблокирован", "user_block_modal_text": "{{displayName}} будет заблокирован",
"blocked_users": "Заблокированные пользователи", "blocked_users": "Заблокированные пользователи",
"unblock": "Разблокировать", "unblock": "Разблокировать",
"no_friends_added": "Вы ещё не добавили ни одного друга", "no_friends_added": "Вы ещё не добавили ни одного друга",
"no_friends_yet": "Вы ещё не добавили ни одного друга",
"view_all": "Показать все",
"load_more": "Загрузить еще",
"loading": "Загрузка",
"pending": "Ожидание", "pending": "Ожидание",
"no_pending_invites": "У вас нет запросов ожидающих ответа", "no_pending_invites": "У вас нет запросов ожидающих ответа",
"no_blocked_users": "Вы не заблокировали ни одного пользователя", "no_blocked_users": "Вы не заблокировали ни одного пользователя",
@@ -721,17 +673,9 @@
"report_reason_spam": "Спам", "report_reason_spam": "Спам",
"report_reason_other": "Другое", "report_reason_other": "Другое",
"profile_reported": "Жалоба на профиль отправлена", "profile_reported": "Жалоба на профиль отправлена",
"your_friend_code": "Ваш код друга:", "your_friend_code": "Код вашего друга:",
"copy_friend_code": "Копировать код друга",
"copied": "Скопировано!",
"upload_banner": "Загрузить баннер", "upload_banner": "Загрузить баннер",
"uploading_banner": "Загрузка баннера...", "uploading_banner": "Загрузка баннера...",
"change_banner": "Изменить баннер",
"replace_banner": "Заменить баннер",
"remove_banner": "Удалить баннер",
"remove_banner_modal_title": "Удалить баннер?",
"remove_banner_confirmation": "Вы уверены, что хотите удалить свой баннер? Вы всегда можете выбрать новый, когда захотите.",
"remove": "Удалить",
"background_image_updated": "Фоновое изображение обновлено", "background_image_updated": "Фоновое изображение обновлено",
"stats": "Статистика", "stats": "Статистика",
"achievements": "Достижения", "achievements": "Достижения",
@@ -749,31 +693,11 @@
"game_added_to_pinned": "Игра добавлена в закрепленные", "game_added_to_pinned": "Игра добавлена в закрепленные",
"karma": "Карма", "karma": "Карма",
"karma_count": "карма", "karma_count": "карма",
"karma_description": "Заработана положительными оценками отзывов",
"user_reviews": "Отзывы", "user_reviews": "Отзывы",
"delete_review": "Удалить отзыв",
"loading_reviews": "Загрузка отзывов...", "loading_reviews": "Загрузка отзывов...",
"wrapped_2025": "Wrapped 2025" "no_reviews": "Пока нет отзывов",
}, "delete_review": "Удалить отзыв"
"library": {
"library": "Библиотека",
"play": "Играть",
"download": "Скачать",
"downloading": "Скачивание",
"game": "игра",
"games": "игры",
"grid_view": "Вид сетки",
"compact_view": "Компактный вид",
"large_view": "Большой вид",
"no_games_title": "Ваша библиотека пуста",
"no_games_description": "Добавьте игры из каталога или скачайте их, чтобы начать",
"amount_hours": "{{amount}} часов",
"amount_minutes": "{{amount}} минут",
"amount_hours_short": "{{amount}}ч",
"amount_minutes_short": "{{amount}}м",
"manual_playtime_tooltip": "Время игры было обновлено вручную",
"all_games": "Все игры",
"recently_played": "Недавно сыгранные",
"favorites": "Избранное"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Достижение разблокировано", "achievement_unlocked": "Достижение разблокировано",
@@ -803,41 +727,5 @@
"hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!", "hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!",
"learn_more": "Подробнее", "learn_more": "Подробнее",
"debrid_description": "Скачивайте в 4 раза быстрее с Nimbus" "debrid_description": "Скачивайте в 4 раза быстрее с Nimbus"
},
"notifications_page": {
"title": "Уведомления",
"mark_all_as_read": "Отметить все как прочитанные",
"clear_all": "Очистить все",
"loading": "Загрузка...",
"empty_title": "Нет уведомлений",
"empty_description": "Вы в курсе всех событий! Загляните позже за новыми обновлениями.",
"empty_filter_description": "Нет уведомлений, соответствующих этому фильтру.",
"filter_all": "Все",
"filter_unread": "Непрочитанные",
"filter_friends": "Друзья",
"filter_badges": "Значки",
"filter_upvotes": "Голоса",
"filter_local": "Локальные",
"load_more": "Загрузить еще",
"dismiss": "Отклонить",
"accept": "Принять",
"refuse": "Отклонить",
"notification": "Уведомление",
"friend_request_received_title": "Новый запрос в друзья!",
"friend_request_received_description": "{{displayName}} хочет добавить вас в друзья",
"friend_request_accepted_title": "Запрос в друзья принят!",
"friend_request_accepted_description": "{{displayName}} принял ваш запрос в друзья",
"badge_received_title": "Вы получили новый значок!",
"badge_received_description": "{{badgeName}}",
"review_upvote_title": "Ваш отзыв на {{gameTitle}} получил голоса!",
"review_upvote_description": "Ваш отзыв получил {{count}} новых голосов",
"marked_all_as_read": "Все уведомления отмечены как прочитанные",
"failed_to_mark_as_read": "Не удалось отметить уведомления как прочитанные",
"cleared_all": "Все уведомления очищены",
"failed_to_clear": "Не удалось очистить уведомления",
"failed_to_load": "Не удалось загрузить уведомления",
"failed_to_dismiss": "Не удалось отклонить уведомление",
"friend_request_accepted": "Запрос в друзья принят",
"friend_request_refused": "Запрос в друзья отклонен"
} }
} }

View File

@@ -1,844 +0,0 @@
{
"language_name": "Slovenščina",
"app": {
"successfully_signed_in": "Uspešno ste se prijavili"
},
"home": {
"surprise_me": "Preseneti me",
"no_results": "Ni najdenih rezultatov",
"start_typing": "Začnite tipkati za iskanje...",
"hot": "Trenutno vroče",
"weekly": "📅 Najboljše igre tedna",
"achievements": "🏆 Igre za premagati"
},
"sidebar": {
"catalogue": "Katalog",
"library": "Knjižnica",
"downloads": "Prenosi",
"settings": "Nastavitve",
"my_library": "Moja knjižnica",
"downloading_metadata": "{{title}} (Prenos metapodatkov…)",
"paused": "{{title}} (V premoru)",
"downloading": "{{title}} ({{percentage}} - Prenos…)",
"filter": "Filtriraj knjižnico",
"home": "Domov",
"queued": "{{title}} (V čakalni vrsti)",
"game_has_no_executable": "Igra nima izbrane izvršljive datoteke",
"sign_in": "Prijavite se",
"friends": "Prijatelji",
"notifications": "Obvestila",
"need_help": "Potrebujete pomoč?",
"favorites": "Priljubljene",
"playable_button_title": "Pokaži le igre, ki jih lahko igrate zdaj",
"add_custom_game_tooltip": "Dodaj igro po meri",
"show_playable_only_tooltip": "Pokaži samo igrljive",
"custom_game_modal": "Dodaj igro po meri",
"custom_game_modal_description": "Dodajte igro po meri v vašo knjižnico z izbiro izvršljive datoteke",
"custom_game_modal_executable_path": "Pot do izvršljive datoteke",
"custom_game_modal_select_executable": "Izberite izvršljivo datoteko",
"custom_game_modal_title": "Naslov",
"custom_game_modal_enter_title": "Vnesite naslov",
"custom_game_modal_browse": "Brskaj",
"custom_game_modal_cancel": "Prekliči",
"custom_game_modal_add": "Dodaj igro",
"custom_game_modal_adding": "Dodajanje igre...",
"custom_game_modal_success": "Igra po meri je bila uspešno dodana",
"custom_game_modal_failed": "Dodajanje igre po meri ni uspelo",
"custom_game_modal_executable": "Izvršljiva datoteka",
"edit_game_modal": "Prilagodi sredstva",
"edit_game_modal_description": "Prilagodite sredstva in podrobnosti igre",
"edit_game_modal_title": "Naslov",
"edit_game_modal_enter_title": "Vnesite naslov",
"edit_game_modal_image": "Slika",
"edit_game_modal_select_image": "Izberite sliko",
"edit_game_modal_browse": "Brskaj",
"edit_game_modal_image_preview": "Predogled slike",
"edit_game_modal_icon": "Ikona",
"edit_game_modal_select_icon": "Izberite ikono",
"edit_game_modal_icon_preview": "Predogled ikone",
"edit_game_modal_logo": "Logotip",
"edit_game_modal_select_logo": "Izberite logotip",
"edit_game_modal_logo_preview": "Predogled logotipa",
"edit_game_modal_hero": "Hero knjižnice",
"edit_game_modal_select_hero": "Izberite sliko hero knjižnice",
"edit_game_modal_hero_preview": "Predogled hero slike knjižnice",
"edit_game_modal_cancel": "Prekliči",
"edit_game_modal_update": "Posodobi",
"edit_game_modal_updating": "Posodabljanje...",
"edit_game_modal_fill_required": "Prosimo, izpolnite vsa obvezna polja",
"edit_game_modal_success": "Sredstva so bila uspešno posodobljena",
"edit_game_modal_failed": "Posodabljanje sredstev ni uspelo",
"edit_game_modal_image_filter": "Slika",
"edit_game_modal_icon_resolution": "Priporočena resolucija: 256x256px",
"edit_game_modal_logo_resolution": "Priporočena resolucija: 640x360px",
"edit_game_modal_hero_resolution": "Priporočena resolucija: 1920x620px",
"edit_game_modal_assets": "Sredstva",
"edit_game_modal_drop_icon_image_here": "Spustite ikono tukaj",
"edit_game_modal_drop_logo_image_here": "Spustite logotip tukaj",
"edit_game_modal_drop_hero_image_here": "Spustite hero sliko tukaj",
"edit_game_modal_drop_to_replace_icon": "Spustite za zamenjavo ikone",
"edit_game_modal_drop_to_replace_logo": "Spustite za zamenjavo logotipa",
"edit_game_modal_drop_to_replace_hero": "Spustite za zamenjavo hero slike",
"install_decky_plugin": "Namesti Decky vtičnik",
"update_decky_plugin": "Posodobi Decky vtičnik",
"decky_plugin_installed_version": "Decky vtičnik (v{{version}})",
"install_decky_plugin_title": "Namesti Hydra Decky vtičnik",
"install_decky_plugin_message": "To bo preneslo in namestilo Hydra vtičnik za Decky Loader. To lahko zahteva povišane pravice. Nadaljujem?",
"update_decky_plugin_title": "Posodobi Hydra Decky vtičnik",
"update_decky_plugin_message": "Na voljo je nova različica Hydra Decky vtičnika. Ali želite posodobiti zdaj?",
"decky_plugin_installed": "Decky vtičnik v{{version}} je bil uspešno nameščen",
"decky_plugin_installation_failed": "Namestitev Decky vtičnika ni uspela: {{error}}",
"decky_plugin_installation_error": "Napaka pri nameščanju Decky vtičnika: {{error}}",
"confirm": "Potrdi",
"cancel": "Prekliči"
},
"header": {
"search": "Išči igre",
"search_library": "Išči v knjižnici",
"recent_searches": "Nedavna iskanja",
"suggestions": "Predlogi",
"clear_history": "Počisti",
"remove_from_history": "Odstrani iz zgodovine",
"loading": "Nalaganje...",
"no_results": "Ni rezultatov",
"home": "Domov",
"catalogue": "Katalog",
"library": "Knjižnica",
"downloads": "Prenosi",
"search_results": "Rezultati iskanja",
"settings": "Nastavitve",
"version_available_install": "Različica {{version}} je na voljo. Kliknite tukaj za ponovni zagon in namestitev.",
"version_available_download": "Različica {{version}} je na voljo. Kliknite tukaj za prenos."
},
"bottom_panel": {
"no_downloads_in_progress": "Ni prenosa v teku",
"downloading_metadata": "Prenos metapodatkov {{title}}…",
"downloading": "Prenos {{title}}… ({{percentage}} končano) - Čas {{eta}} - {{speed}}",
"calculating_eta": "Prenos {{title}}… ({{percentage}} končano) - Izračun preostalega časa…",
"checking_files": "Preverjanje datotek {{title}}… ({{percentage}} končano)",
"extracting": "Razpakiranje {{title}}… ({{percentage}} končano)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Namestitev zaključena",
"installation_complete_message": "Skupni redistributables so bili uspešno nameščeni"
},
"catalogue": {
"search": "Filtriraj…",
"developers": "Razvijalci",
"genres": "Žanri",
"tags": "Oznake",
"publishers": "Izdajatelji",
"download_sources": "Viri prenosa",
"result_count": "{{resultCount}} rezultatov",
"filter_count": "{{filterCount}} na voljo",
"clear_filters": "Počisti {{filterCount}} izbranih"
},
"game_details": {
"open_download_options": "Odpri možnosti prenosa",
"download_options_zero": "Ni možnosti prenosa",
"download_options_one": "{{count}} možnost prenosa",
"download_options_other": "{{count}} možnosti prenosa",
"updated_at": "Posodobljeno {{updated_at}}",
"install": "Namesti",
"resume": "Nadaljuj",
"pause": "Premor",
"cancel": "Prekliči",
"remove": "Odstrani",
"space_left_on_disk": "{{space}} prosto na disku",
"eta": "Zaključek {{eta}}",
"calculating_eta": "Izračun preostalega časa…",
"downloading_metadata": "Prenos metapodatkov…",
"filter": "Filtriraj repake",
"requirements": "Sistemske zahteve",
"minimum": "Minimum",
"recommended": "Priporočeno",
"paused": "V premoru",
"release_date": "Izid dne {{date}}",
"publisher": "Objavljeno s strani {{publisher}}",
"hours": "ur",
"minutes": "minut",
"amount_hours": "{{amount}} ur",
"amount_minutes": "{{amount}} minut",
"accuracy": "{{accuracy}}% natančnost",
"add_to_library": "Dodaj v knjižnico",
"already_in_library": "Že v knjižnici",
"remove_from_library": "Odstrani iz knjižnice",
"no_downloads": "Ni razpoložljivih prenosov",
"play_time": "Odigrano {{amount}}",
"last_time_played": "Nazadnje igrano {{period}}",
"not_played_yet": "Še niste igrali {{title}}",
"next_suggestion": "Naslednji predlog",
"play": "Igraj",
"deleting": "Brisanje namestitvenega programa…",
"close": "Zapri",
"playing_now": "Trenutno igranje",
"change": "Spremeni",
"repacks_modal_description": "Izberite repak, ki ga želite prenesti",
"select_folder_hint": "Za spremembo privzete mape pojdite v <0>Nastavitve</0>",
"download_now": "Prenesi zdaj",
"loading": "Nalaganje...",
"no_shop_details": "Podatkov o trgovini ni bilo mogoče pridobiti.",
"download_options": "Možnosti prenosa",
"download_path": "Pot prenosa",
"previous_screenshot": "Prejšnji posnetek zaslona",
"next_screenshot": "Naslednji posnetek zaslona",
"screenshot": "Posnetek zaslona {{number}}",
"open_screenshot": "Odpri posnetek zaslona {{number}}",
"download_settings": "Nastavitve prenosa",
"downloader": "Prenosnik",
"downloader_online": "Spletno",
"downloader_not_configured": "Na voljo, vendar ni nastavljeno",
"downloader_offline": "Povezava je brez povezave",
"downloader_not_available": "Ni na voljo",
"recommended": "Priporočeno",
"go_to_settings": "Pojdi v nastavitve",
"select_executable": "Izberi",
"no_executable_selected": "Ni izbrane izvršljive datoteke",
"open_folder": "Odpri mapo",
"open_download_location": "Poglej prenesene datoteke",
"create_shortcut": "Ustvari bližnjico na namizju",
"create_shortcut_simple": "Ustvari bližnjico",
"clear": "Počisti",
"remove_files": "Odstrani datoteke",
"remove_from_library_title": "Ali ste prepričani?",
"remove_from_library_description": "To bo odstranilo {{game}} iz vaše knjižnice",
"options": "Možnosti",
"properties": "Lastnosti",
"executable_section_title": "Izvršljiva datoteka",
"executable_section_description": "Pot do datoteke, ki se bo izvedla ob kliku na \"Igraj\"",
"downloads_section_title": "Prenosi",
"downloads_section_description": "Preverite posodobitve ali druge različice te igre",
"danger_zone_section_title": "Nevarno območje",
"danger_zone_section_description": "Odstranite to igro iz knjižnice ali datoteke, ki jih je prenesel Hydra",
"download_in_progress": "Prenos v teku",
"download_paused": "Prenos v premoru",
"extracting": "Razpakiranje",
"last_downloaded_option": "Zadnja prenesena možnost",
"new_download_option": "Novo",
"create_steam_shortcut": "Ustvari Steam bližnjico",
"create_shortcut_success": "Bližnjica je bila uspešno ustvarjena",
"you_might_need_to_restart_steam": "Morda boste morali ponovno zagnati Steam, da vidite spremembe",
"create_shortcut_error": "Napaka pri ustvarjanju bližnjice",
"add_to_favorites": "Dodaj med priljubljene",
"remove_from_favorites": "Odstrani iz priljubljenih",
"failed_update_favorites": "Posodabljanje priljubljenih ni uspelo",
"game_removed_from_library": "Igra odstranjena iz knjižnice",
"failed_remove_from_library": "Odstranjevanje iz knjižnice ni uspelo",
"files_removed_success": "Datoteke so bile uspešno odstranjene",
"failed_remove_files": "Odstranjevanje datotek ni uspelo",
"nsfw_content_title": "Ta igra vsebuje neprimerno vsebino",
"nsfw_content_description": "{{title}} vsebuje vsebino, ki morda ni primerna za vse starosti. Ali ste prepričani, da želite nadaljevati?",
"allow_nsfw_content": "Nadaljuj",
"refuse_nsfw_content": "Nazaj",
"stats": "Statistika",
"download_count": "Prenosi",
"player_count": "Aktivni igralci",
"rating_count": "Ocena",
"download_error": "Ta možnost prenosa ni na voljo",
"download": "Prenesi",
"executable_path_in_use": "Izvršljiva datoteka že uporablja \"{{game}}\"",
"warning": "Opozorilo:",
"hydra_needs_to_remain_open": "Za ta prenos mora Hydra ostati odprta, dokler ni končana. Če se Hydra zapre pred končanim prenosom, boste izgubili napredek.",
"achievements": "Dosežki",
"achievements_count": "Dosežki {{unlockedCount}}/{{achievementsCount}}",
"show_more": "Pokaži več",
"show_less": "Pokaži manj",
"reviews": "Mnenja",
"review_played_for": "Odigrano za",
"leave_a_review": "Oddajte mnenje",
"write_review_placeholder": "Delite svoje misli o tej igri...",
"sort_newest": "Najnovejše",
"no_reviews_yet": "Ni še mnenj",
"be_first_to_review": "Bodite prvi, ki delite svoje misli o tej igri!",
"sort_oldest": "Najstarejše",
"sort_highest_score": "Najvišja ocena",
"sort_lowest_score": "Najnižja ocena",
"sort_most_voted": "Največ glasov",
"rating": "Ocena",
"rating_stats": "Ocena",
"rating_very_negative": "Zelo negativno",
"rating_negative": "Negativno",
"rating_neutral": "Nevtralno",
"rating_positive": "Pozitivno",
"rating_very_positive": "Zelo pozitivno",
"submit_review": "Pošlji",
"submitting": "Pošiljanje...",
"review_submitted_successfully": "Mnenje je bilo uspešno poslano!",
"review_submission_failed": "Pošiljanje mnenja ni uspelo. Prosimo, poskusite znova.",
"review_cannot_be_empty": "Polje mnenja ne sme biti prazno.",
"review_deleted_successfully": "Mnenje je bilo uspešno izbrisano.",
"review_deletion_failed": "Brisanje mnenja ni uspelo. Prosimo, poskusite znova.",
"loading_reviews": "Nalagam mnenja...",
"loading_more_reviews": "Nalagam več mnenj...",
"load_more_reviews": "Naloži več mnenj",
"you_seemed_to_enjoy_this_game": "Zdi se, da uživate v tej igri",
"would_you_recommend_this_game": "Bi radi oddali mnenje o tej igri?",
"yes": "Da",
"maybe_later": "Mogoče kasneje",
"cloud_save": "Shranjevanje v oblaku",
"cloud_save_description": "Shranjujte napredek v oblak in nadaljujte igranje na katerikoli napravi",
"backups": "Varnostne kopije",
"install_backup": "Namesti",
"delete_backup": "Izbriši",
"create_backup": "Nova varnostna kopija",
"last_backup_date": "Zadnja varnostna kopija {{date}}",
"no_backup_preview": "Ni shranjenih iger za ta naslov",
"restoring_backup": "Obnavljanje varnostne kopije ({{progress}} končano)…",
"uploading_backup": "Nalaganje varnostne kopije…",
"no_backups": "Za to igro še niste ustvarili varnostnih kopij",
"backup_uploaded": "Varnostna kopija naložena",
"backup_failed": "Varnostna kopija ni uspela",
"backup_deleted": "Varnostna kopija izbrisana",
"backup_restored": "Varnostna kopija obnovljena",
"see_all_achievements": "Poglej vse dosežke",
"sign_in_to_see_achievements": "Prijavite se za ogled dosežkov",
"mapping_method_automatic": "Samodejno",
"mapping_method_manual": "Ročno",
"mapping_method_label": "Način mapiranja",
"files_automatically_mapped": "Datoteke so samodejno preslikane",
"no_backups_created": "Za to igro ni ustvarjenih varnostnih kopij",
"manage_files": "Upravljaj datoteke",
"loading_save_preview": "Iskanje shranjenih iger…",
"wine_prefix": "Wine predpona",
"wine_prefix_description": "Wine predpona, uporabljena za zagon te igre",
"launch_options": "Možnosti zagona",
"launch_options_description": "Napredni uporabniki lahko vpišejo spremembe v možnosti zagona (eksperimentalna funkcija)",
"launch_options_placeholder": "Ni določenega parametra",
"no_download_option_info": "Ni razpoložljivih informacij",
"backup_deletion_failed": "Brisanje varnostne kopije ni uspelo",
"max_number_of_artifacts_reached": "Doseženo je največje število varnostnih kopij za to igro",
"achievements_not_sync": "Oglejte si, kako sinhronizirati svoje dosežke",
"manage_files_description": "Upravljajte, katere datoteke bodo varnostno kopirane in obnovljene",
"select_folder": "Izberite mapo",
"backup_from": "Varnostna kopija od {{date}}",
"automatic_backup_from": "Samodejna varnostna kopija od {{date}}",
"enable_automatic_cloud_sync": "Omogoči samodejno sinhronizacijo v oblaku",
"custom_backup_location_set": "Nastavljena je po meri lokacija varnostne kopije",
"no_directory_selected": "Ni izbrane mape",
"no_write_permission": "V to mapo ni mogoče prenesti. Kliknite tukaj za več informacij.",
"reset_achievements": "Ponastavi dosežke",
"reset_achievements_description": "To bo ponastavilo vse dosežke za {{game}}",
"reset_achievements_title": "Ali ste prepričani?",
"reset_achievements_success": "Dosežki so bili uspešno ponastavljeni",
"reset_achievements_error": "Ponastavitev dosežkov ni uspela",
"download_error_gofile_quota_exceeded": "Presegli ste mesečno kvoto Gofile. Prosimo, počakajte, da se kvota ponastavi.",
"download_error_real_debrid_account_not_authorized": "Vaš račun Real-Debrid ni pooblaščen za nove prenose. Preverite nastavitve računa in poskusite znova.",
"download_error_not_cached_on_real_debrid": "Ta prenos ni na voljo v Real-Debrid in preverjanje statusa prenosa iz Real-Debrid še ni na voljo.",
"update_playtime_title": "Posodobi čas igranja",
"update_playtime_description": "Ročno posodobite čas igranja za {{game}}",
"update_playtime": "Posodobi čas igranja",
"update_playtime_success": "Čas igranja je bil uspešno posodobljen",
"update_playtime_error": "Posodabljanje časa igranja ni uspelo",
"update_game_playtime": "Posodobi čas igranja igre",
"manual_playtime_warning": "Vaše ure bodo označene kot ročno posodobljene, tega ni mogoče razveljaviti.",
"manual_playtime_tooltip": "Ta čas igranja je bil ročno posodobljen",
"download_error_not_cached_on_torbox": "Ta prenos ni na voljo v TorBox in preverjanje statusa prenosa iz TorBox še ni na voljo.",
"download_error_not_cached_on_hydra": "Ta prenos ni na voljo v Nimbus.",
"game_removed_from_favorites": "Igra odstranjena iz priljubljenih",
"game_added_to_favorites": "Igra dodana med priljubljene",
"game_removed_from_pinned": "Igra odstranjena iz pripetih",
"game_added_to_pinned": "Igra pripeta",
"automatically_extract_downloaded_files": "Samodejno razpakiraj prenesene datoteke",
"create_start_menu_shortcut": "Ustvari bližnjico v Start meniju",
"invalid_wine_prefix_path": "Neveljavna pot Wine predpone",
"invalid_wine_prefix_path_description": "Pot do Wine predpone je neveljavna. Preverite pot in poskusite znova.",
"missing_wine_prefix": "Wine predpona je potrebna za ustvarjanje varnostne kopije na Linuxu",
"artifact_renamed": "Varnostna kopija je bila uspešno preimenovana",
"rename_artifact": "Preimenuj varnostno kopijo",
"rename_artifact_description": "Preimenujte varnostno kopijo v bolj opisno ime",
"artifact_name_label": "Ime varnostne kopije",
"artifact_name_placeholder": "Vnesite ime varnostne kopije",
"save_changes": "Shrani spremembe",
"required_field": "To polje je obvezno",
"max_length_field": "To polje mora biti krajše od {{length}} znakov",
"freeze_backup": "Pripni, da jo samodejne varnostne kopije ne prepišejo",
"unfreeze_backup": "Odpni",
"backup_frozen": "Varnostna kopija je pripeta",
"backup_unfrozen": "Varnostna kopija je odprijeta",
"backup_freeze_failed": "Pripenjanje varnostne kopije ni uspelo",
"backup_freeze_failed_description": "Morate pustiti vsaj en prost prostor za samodejne varnostne kopije",
"edit_game_modal_button": "Prilagodi sredstva igre",
"game_details": "Podrobnosti igre",
"currency_symbol": "$",
"currency_country": "us",
"prices": "Cene",
"no_prices_found": "Ni najdenih cen",
"view_all_prices": "Kliknite za ogled vseh cen",
"retail_price": "Maloprodajna cena",
"keyshop_price": "Cena v trgovini s ključki",
"historical_retail": "Zgodovinska maloprodajna cena",
"historical_keyshop": "Zgodovinska cena v trgovini s ključki",
"language": "Jezik",
"caption": "Naslov",
"audio": "Zvok",
"filter_by_source": "Filtriraj po viru",
"no_repacks_found": "Za to igro ni najdenih virov",
"delete_review": "Izbriši mnenje",
"remove_review": "Odstrani mnenje",
"delete_review_modal_title": "Ali ste prepričani, da želite izbrisati svoje mnenje?",
"delete_review_modal_description": "Tega dejanja ni mogoče razveljaviti.",
"delete_review_modal_delete_button": "Izbriši",
"delete_review_modal_cancel_button": "Prekliči",
"vote_failed": "Glasovanja ni uspelo. Prosimo, poskusite znova.",
"show_original": "Pokaži original",
"show_translation": "Pokaži prevod",
"show_original_translated_from": "Pokaži original (prevedeno iz {{language}})",
"hide_original": "Skrij original",
"review_from_blocked_user": "Mnenje blokiranega uporabnika",
"show": "Pokaži",
"hide": "Skrij"
},
"activation": {
"title": "Aktiviraj Hydra",
"installation_id": "ID namestitve:",
"enter_activation_code": "Vnesite aktivacijsko kodo",
"message": "Če ne veste, kje naj to pridobite, potem tega ne bi smeli imeti.",
"activate": "Aktiviraj",
"loading": "Nalaganje…"
},
"downloads": {
"resume": "Nadaljuj",
"pause": "Premor",
"eta": "Zaključek {{eta}}",
"paused": "V premoru",
"verifying": "Preverjanje…",
"completed": "Dokončano",
"removed": "Ni preneseno",
"cancel": "Prekliči",
"cancel_download": "Prekliči prenos?",
"cancel_download_description": "Ali ste prepričani, da želite prekiniti ta prenos? Vse prenesene datoteke bodo izbrisane.",
"keep_downloading": "Ne, nadaljuj prenos",
"yes_cancel": "Da, prekliči",
"filter": "Filtriraj prenesene igre",
"remove": "Odstrani",
"downloading_metadata": "Prenos metapodatkov…",
"deleting": "Brisanje namestitvenega programa…",
"delete": "Odstrani namestitveni program",
"delete_modal_title": "Ali ste prepričani?",
"delete_modal_description": "To bo odstranilo vse namestitvene datoteke z računalnika",
"install": "Namesti",
"download_in_progress": "V teku",
"queued_downloads": "Prenosi v čakalni vrsti",
"downloads_completed": "Dokončano",
"queued": "V čakalni vrsti",
"no_downloads_title": "Tako prazno",
"no_downloads_description": "Še niste prenesli ničesar z Hydra, a nikoli ni prepozno začeti.",
"checking_files": "Preverjanje datotek…",
"seeding": "Sejanje",
"stop_seeding": "Ustavi sejanje",
"resume_seeding": "Nadaljuj sejanje",
"options": "Upravljaj",
"extract": "Razpakiraj datoteke",
"extracting": "Razpakiranje datotek…",
"delete_archive_title": "Ali želite izbrisati {{fileName}}?",
"delete_archive_description": "Datoteka je bila uspešno razpakirana in ni več potrebna.",
"yes": "Da",
"no": "Ne",
"network": "OMREŽJE",
"peak": "VRH"
},
"settings": {
"downloads_path": "Pot prenosa",
"change": "Posodobi",
"notifications": "Obvestila",
"enable_download_notifications": "Ko je prenos končan",
"enable_repack_list_notifications": "Ko je dodan nov repack",
"real_debrid_api_token_label": "Real-Debrid API žeton",
"quit_app_instead_hiding": "Ne skrij Hydre pri zapiranju",
"launch_with_system": "Zaženi Hydra ob zagonu sistema",
"general": "Splošno",
"behavior": "Obnašanje",
"download_sources": "Viri prenosa",
"language": "Jezik",
"api_token": "API žeton",
"enable_real_debrid": "Omogoči Real-Debrid",
"real_debrid_description": "Real-Debrid je neomejen prenašalnik, ki vam omogoča hitro prenašanje datotek, omejeno le s hitrostjo vašega interneta.",
"debrid_invalid_token": "Neveljaven API žeton",
"debrid_api_token_hint": "Žeton API lahko dobite <0>tukaj</0>",
"real_debrid_free_account_error": "Račun \"{{username}}\" je brezplačen. Prosimo, naročite se na Real-Debrid",
"debrid_linked_message": "Račun \"{{username}}\" povezan",
"save_changes": "Shrani spremembe",
"changes_saved": "Spremembe uspešno shranjene",
"download_sources_description": "Hydra bo pridobila povezave za prenos iz teh virov. URL vira mora biti neposredna povezava do .json datoteke, ki vsebuje povezave za prenos.",
"validate_download_source": "Preveri",
"remove_download_source": "Odstrani",
"add_download_source": "Dodaj vir",
"adding": "Dodajanje…",
"failed_add_download_source": "Dodajanje vira za prenos ni uspelo. Poskusite znova.",
"download_source_already_exists": "Ta URL vira za prenos že obstaja.",
"download_count_zero": "Ni možnosti prenosa",
"download_count_one": "{{countFormatted}} možnost prenosa",
"download_count_other": "{{countFormatted}} možnosti prenosa",
"download_source_url": "URL vira za prenos",
"add_download_source_description": "Vstavite URL .json datoteke",
"download_source_up_to_date": "Posodobljeno",
"download_source_errored": "Napaka",
"download_source_pending_matching": "Kmalu posodobljeno",
"download_source_matched": "Posodobljeno",
"download_source_matching": "Posodabljanje",
"download_source_failed": "Napaka",
"download_source_no_information": "Ni podatkov na voljo",
"sync_download_sources": "Sinhroniziraj vire",
"removed_download_source": "Vir prenosa odstranjen",
"removed_download_sources": "Viri prenosa odstranjeni",
"removed_all_download_sources": "Vsi viri prenosa odstranjeni",
"download_sources_synced_successfully": "Vsi viri prenosa so sinhronizirani",
"cancel_button_confirmation_delete_all_sources": "Ne",
"confirm_button_confirmation_delete_all_sources": "Da, izbriši vse",
"title_confirmation_delete_all_sources": "Izbriši vse vire prenosa",
"description_confirmation_delete_all_sources": "Izbrišete vse vire prenosa",
"button_delete_all_sources": "Odstrani vse",
"added_download_source": "Vir prenosa dodan",
"download_sources_synced": "Vsi viri prenosa so sinhronizirani",
"insert_valid_json_url": "Vnesite veljaven JSON URL",
"found_download_option_zero": "Ni možnosti prenosa",
"found_download_option_one": "Najdena {{countFormatted}} možnost prenosa",
"found_download_option_other": "Najdenih {{countFormatted}} možnosti prenosa",
"import": "Uvozi",
"importing": "Uvažanje...",
"public": "Javno",
"private": "Zasebno",
"friends_only": "Samo prijatelji",
"privacy": "Zasebnost",
"profile_visibility": "Vidnost profila",
"profile_visibility_description": "Izberite, kdo lahko vidi vaš profil in knjižnico",
"required_field": "To polje je obvezno",
"source_already_exists": "Ta vir je že bil dodan",
"must_be_valid_url": "Vir mora biti veljaven URL",
"blocked_users": "Blokirani uporabniki",
"user_unblocked": "Uporabnik je odblokiran",
"enable_achievement_notifications": "Ko je dosežek odklenjen",
"launch_minimized": "Zaženi Hydra minimizirano",
"disable_nsfw_alert": "Onemogoči opozorilo NSFW",
"seed_after_download_complete": "Sejanje po končanem prenosu",
"show_hidden_achievement_description": "Pokaži opis skritih dosežkov pred njihovim odklepanjem",
"account": "Račun",
"hydra_cloud": "Hydra Cloud",
"no_users_blocked": "Nimate blokiranih uporabnikov",
"subscription_active_until": "Vaš Hydra Cloud je aktiven do {{date}}",
"manage_subscription": "Upravljaj naročnino",
"update_email": "Posodobi e-pošto",
"update_password": "Posodobi geslo",
"current_email": "Trenutna e-pošta:",
"no_email_account": "Še niste nastavili e-pošte",
"account_data_updated_successfully": "Podatki računa so bili uspešno posodobljeni",
"renew_subscription": "Obnovi Hydra Cloud",
"subscription_expired_at": "Vaša naročnina je potekla {{date}}",
"no_subscription": "Uživajte v Hydri na najboljši način",
"become_subscriber": "Postanite Hydra Cloud uporabnik",
"subscription_renew_cancelled": "Samodejno podaljševanje je onemogočeno",
"subscription_renews_on": "Vaša naročnina se podaljša {{date}}",
"bill_sent_until": "Naslednji račun bo poslan do tega dne",
"no_themes": "Zdi se, da še nimate tem, vendar brez skrbi, kliknite tukaj, da ustvarite svojo prvo mojstrovino.",
"editor_tab_code": "Koda",
"editor_tab_info": "Info",
"editor_tab_save": "Shrani",
"web_store": "Spletna trgovina",
"clear_themes": "Počisti",
"create_theme": "Ustvari",
"create_theme_modal_title": "Ustvari prilagojeno temo",
"create_theme_modal_description": "Ustvarite novo temo za prilagajanje videza Hydre",
"theme_name": "Ime",
"insert_theme_name": "Vstavite ime teme",
"set_theme": "Nastavi temo",
"unset_theme": "Odstrani temo",
"delete_theme": "Izbriši temo",
"edit_theme": "Uredi temo",
"delete_all_themes": "Izbriši vse teme",
"delete_all_themes_description": "To bo izbrisalo vse vaše prilagojene teme",
"delete_theme_description": "To bo izbrisalo temo {{theme}}",
"cancel": "Prekliči",
"appearance": "Videz",
"debrid": "Debrid",
"debrid_description": "Debrid storitve so premium neomejeni prenašalniki, ki vam omogočajo hitro prenašanje datotek, gostovanih na različnih storitvah za gostovanje datotek, omejeno le s hitrostjo vašega interneta.",
"enable_torbox": "Omogoči TorBox",
"torbox_description": "TorBox je vaša premium seedbox storitev, ki se lahko kosuje tudi najboljšim strežnikom na trgu.",
"torbox_account_linked": "TorBox račun povezan",
"create_real_debrid_account": "Kliknite tukaj, če še nimate Real-Debrid računa",
"create_torbox_account": "Kliknite tukaj, če še nimate TorBox računa",
"real_debrid_account_linked": "Real-Debrid račun povezan",
"name_min_length": "Ime teme mora imeti vsaj 3 znake",
"import_theme": "Uvozi temo",
"import_theme_description": "Uvožili boste {{theme}} iz trgovine tem",
"error_importing_theme": "Napaka pri uvozu teme",
"theme_imported": "Tema uspešno uvožena",
"enable_friend_request_notifications": "Ko je prejet prijateljski zahtevek",
"enable_auto_install": "Samodejno prenesi posodobitve",
"common_redist": "Skupni redistributable-ji",
"common_redist_description": "Skupni redistributable-ji so potrebni za zagon nekaterih iger. Priporočamo njihovo namestitev, da se izognete težavam.",
"install_common_redist": "Namesti",
"installing_common_redist": "Nameščanje…",
"show_download_speed_in_megabytes": "Pokaži hitrost prenosa v megabajtih na sekundo",
"extract_files_by_default": "Privzeto razpakiraj datoteke po prenosu",
"enable_steam_achievements": "Omogoči iskanje po Steam dosežkih",
"enable_new_download_options_badges": "Pokaži značke novih možnosti prenosa",
"achievement_custom_notification_position": "Lastna pozicija obvestil o dosežkih",
"top-left": "Zgoraj levo",
"top-center": "Zgoraj na sredini",
"top-right": "Zgoraj desno",
"bottom-left": "Spodaj levo",
"bottom-center": "Spodaj na sredini",
"bottom-right": "Spodaj desno",
"enable_achievement_custom_notifications": "Omogoči lastna obvestila o dosežkih",
"alignment": "Poravnava",
"variation": "Variacija",
"default": "Privzeto",
"rare": "Redko",
"platinum": "Platinasto",
"hidden": "Skrito",
"test_notification": "Preizkusno obvestilo",
"achievement_sound_volume": "Glasnost zvoka dosežka",
"select_achievement_sound": "Izberite zvok dosežka",
"change_achievement_sound": "Spremeni zvok dosežka",
"remove_achievement_sound": "Odstrani zvok dosežka",
"preview_sound": "Predogled zvoka",
"select": "Izberi",
"preview": "Predogled",
"remove": "Odstrani",
"no_sound_file_selected": "Nobena zvočna datoteka ni izbrana",
"notification_preview": "Predogled obvestila o dosežku",
"enable_friend_start_game_notifications": "Ko prijatelj začne igrati igro",
"autoplay_trailers_on_game_page": "Samodejno predvajaj napovednike na strani igre",
"hide_to_tray_on_game_start": "Skrij Hydreo v sistemsko vrstico ob zagonu igre",
"downloads": "Prenosi",
"use_native_http_downloader": "Uporabi izvorni HTTP prenašalnik (eksperimentalno)",
"cannot_change_downloader_while_downloading": "Nastavitve ni mogoče spremeniti med prenosom",
"notifications": {
"download_complete": "Prenos končan",
"game_ready_to_install": "{{title}} je pripravljen za namestitev",
"repack_list_updated": "Seznam repackov posodobljen",
"repack_count_one": "{{count}} repack dodan",
"repack_count_other": "{{count}} repackov dodanih",
"new_update_available": "Različica {{version}} na voljo",
"restart_to_install_update": "Znova zaženite Hydreo za namestitev posodobitve",
"notification_achievement_unlocked_title": "Dosežek odklenjen za {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} in drugi {{count}} so bili odklenjeni",
"new_friend_request_description": "{{displayName}} vam je poslal prijateljsko zahtevo",
"new_friend_request_title": "Nova prijateljska zahteva",
"extraction_complete": "Razpakiranje končano",
"game_extracted": "{{title}} je bil uspešno razpakiran",
"friend_started_playing_game": "{{displayName}} je začel igrati igro",
"test_achievement_notification_title": "To je preizkusno obvestilo",
"test_achievement_notification_description": "Kar kul, kajne?"
},
"system_tray": {
"open": "Odpri Hydreo",
"quit": "Izhod"
},
"game_card": {
"available_one": "Na voljo",
"available_other": "Na voljo",
"no_downloads": "Ni razpoložljivih prenosov",
"calculating": "Računam"
},
"binary_not_found_modal": {
"title": "Programi niso nameščeni",
"description": "Izvajalniki Wine ali Lutris niso bili najdeni na vašem sistemu",
"instructions": "Preverite pravi način za namestitev katerega od njih na vašo Linux distribucijo, da bi igra lahko normalno tekla"
},
"modal": {
"close": "Zapri gumb"
},
"forms": {
"toggle_password_visibility": "Preklopi vidnost gesla"
},
"user_profile": {
"amount_hours": "{{amount}} ur",
"amount_minutes": "{{amount}} minut",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"last_time_played": "Nazadnje igrano {{period}}",
"activity": "Nedavna dejavnost",
"library": "Knjižnica",
"pinned": "Pripeto",
"sort_by": "Razvrsti po:",
"achievements_earned": "Odklenjeni dosežki",
"played_recently": "Nazadnje igrano",
"playtime": "Čas igranja",
"total_play_time": "Skupni čas igranja",
"manual_playtime_tooltip": "Ta čas igranja je bil ročno posodobljen",
"no_recent_activity_title": "Hmmm… nič tukaj",
"no_recent_activity_description": "Niste igrali nobene igre v zadnjem času. Čas je, da to spremenite!",
"display_name": "Prikazno ime",
"saving": "Shranjevanje",
"save": "Shrani",
"edit_profile": "Uredi profil",
"saved_successfully": "Uspešno shranjeno",
"try_again": "Prosimo, poskusite znova",
"sign_out_modal_title": "Ste prepričani?",
"cancel": "Prekliči",
"successfully_signed_out": "Uspešno odjavljeni",
"sign_out": "Odjavi se",
"playing_for": "Igra za {{amount}}",
"sign_out_modal_text": "Vaša knjižnica je povezana s trenutnim računom. Ob odjavi knjižnica ne bo več vidna, napredek pa se ne bo shranil. Nadaljujete z odjavo?",
"add_friends": "Dodaj prijatelje",
"add": "Dodaj",
"friend_code": "Koda prijatelja",
"see_profile": "Poglej profil",
"sending": "Pošiljanje",
"friend_request_sent": "Zahteva za prijateljstvo poslana",
"friends": "Prijatelji",
"badges": "Značke",
"friends_list": "Seznam prijateljev",
"user_not_found": "Uporabnik ni najden",
"block_user": "Blokiraj uporabnika",
"add_friend": "Dodaj prijatelja",
"request_sent": "Zahteva poslana",
"request_received": "Zahteva prejeta",
"accept_request": "Sprejmi zahtevo",
"ignore_request": "Ignoriraj zahtevo",
"cancel_request": "Prekliči zahtevo",
"undo_friendship": "Razveljavi prijateljstvo",
"friendship_removed": "Prijatelj odstranjen",
"request_accepted": "Zahteva sprejeta",
"user_blocked_successfully": "Uporabnik uspešno blokiran",
"user_block_modal_text": "To bo blokiralo {{displayName}}",
"blocked_users": "Blokirani uporabniki",
"unblock": "Odblokiraj",
"no_friends_added": "Nimate dodanih prijateljev",
"no_friends_yet": "Še niste dodali prijateljev",
"view_all": "Poglej vse",
"load_more": "Naloži več",
"loading": "Nalaganje",
"pending": "V teku",
"no_pending_invites": "Nimate čakajočih povabil",
"no_blocked_users": "Nimate blokiranih uporabnikov",
"friend_code_copied": "Koda prijatelja kopirana",
"undo_friendship_modal_text": "To bo razveljavilo vaše prijateljstvo z {{displayName}}",
"privacy_hint": "Za prilagoditev, kdo to vidi, pojdite na <0>Nastavitve</0>",
"locked_profile": "Ta profil je zaseben",
"image_process_failure": "Napaka pri obdelavi slike",
"required_field": "To polje je obvezno",
"displayname_min_length": "Prikazno ime mora biti dolgo vsaj 3 znake",
"displayname_max_length": "Prikazno ime mora imeti največ 50 znakov",
"report_profile": "Prijavi ta profil",
"report_reason": "Zakaj prijavljate ta profil?",
"report_description": "Dodatne informacije",
"report_description_placeholder": "Dodatne informacije",
"report": "Prijavi",
"report_reason_hate": "Sovražni govor",
"report_reason_sexual_content": "Seksualna vsebina",
"report_reason_violence": "Nasilje",
"report_reason_spam": "Spam",
"report_reason_other": "Drugo",
"profile_reported": "Profil prijavljen",
"your_friend_code": "Vaša koda prijatelja:",
"copy_friend_code": "Kopiraj kodo prijatelja",
"copied": "Kopirano!",
"upload_banner": "Naloži banner",
"uploading_banner": "Nalaganje bannerja…",
"change_banner": "Spremeni banner",
"replace_banner": "Zamenjaj banner",
"remove_banner": "Odstrani banner",
"remove_banner_modal_title": "Odstrani banner?",
"remove_banner_confirmation": "Ali ste prepričani, da želite odstraniti banner? Kadarkoli lahko izberete novega.",
"remove": "Odstrani",
"background_image_updated": "Pozadinska slika posodobljena",
"stats": "Statistika",
"achievements": "dosežki",
"games": "Igre",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Uvrstitev se posodablja tedensko",
"playing": "Igra {{game}}",
"achievements_unlocked": "Dosežki odklenjeni",
"earned_points": "Zaslužene točke",
"show_achievements_on_profile": "Pokaži vaše dosežke na profilu",
"show_points_on_profile": "Pokaži vaše zaslužene točke na profilu",
"error_adding_friend": "Zahteve za prijatelja ni bilo mogoče poslati. Preverite kodo prijatelja",
"friend_code_length_error": "Koda prijatelja mora vsebovati 8 znakov",
"game_removed_from_pinned": "Igra odstranjena iz pripetih",
"game_added_to_pinned": "Igra dodana med pripete",
"karma": "Karma",
"karma_count": "karma",
"user_reviews": "Mnenja",
"delete_review": "Izbriši mnenje",
"loading_reviews": "Nalaganje mnenj...",
"wrapped_2025": "Wrapped 2025"
},
"library": {
"library": "Knjižnica",
"play": "Igraj",
"download": "Prenesi",
"downloading": "Prenašanje",
"game": "igra",
"games": "igre",
"grid_view": "Mrežni pogled",
"compact_view": "Kompaktni pogled",
"large_view": "Velik pogled",
"no_games_title": "Vaša knjižnica je prazna",
"no_games_description": "Dodajte igre iz kataloga ali jih prenesite, da začnete",
"amount_hours": "{{amount}} ur",
"amount_minutes": "{{amount}} minut",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "Ta čas igranja je bil ročno posodobljen",
"all_games": "Vse igre",
"recently_played": "Nedavno igrane",
"favorites": "Priljubljene"
},
"achievement": {
"achievement_unlocked": "Dosežek odklenjen",
"user_achievements": "Dosežki uporabnika {{displayName}}",
"your_achievements": "Vaši dosežki",
"unlocked_at": "Odklenjeno: {{date}}",
"subscription_needed": "Naročnina na Hydra Cloud je potrebna za ogled te vsebine",
"new_achievements_unlocked": "Odklenili ste {{achievementCount}} novih dosežkov iz {{gameCount}} iger",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} dosežkov",
"achievements_unlocked_for_game": "Odklenili ste {{achievementCount}} novih dosežkov za {{gameTitle}}",
"hidden_achievement_tooltip": "To je skriti dosežek",
"achievement_earn_points": "Z zaslužite {{points}} točk s tem dosežkom",
"earned_points": "Zaslužene točke:",
"available_points": "Razpoložljive točke:",
"how_to_earn_achievements_points": "Kako zaslužiti točke za dosežke?"
},
"hydra_cloud": {
"subscription_tour_title": "Naročnina Hydra Cloud",
"subscribe_now": "Naroči se zdaj",
"cloud_saving": "Shranjevanje v oblak",
"cloud_achievements": "Shrani svoje dosežke v oblak",
"animated_profile_picture": "Animirane profilne slike",
"premium_support": "Premium podpora",
"show_and_compare_achievements": "Pokaži in primerjaj svoje dosežke z drugimi uporabniki",
"animated_profile_banner": "Animirani profilni banner",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Pravkar ste odkrili funkcijo Hydra Cloud!",
"learn_more": "Več informacij",
"debrid_description": "Prenesite do 4x hitreje z Nimbusom"
},
"notifications_page": {
"title": "Obvestila",
"mark_all_as_read": "Označi vse kot prebrano",
"clear_all": "Počisti vse",
"loading": "Nalagam...",
"empty_title": "Ni obvestil",
"empty_description": "Ste na tekočem! Preverite kasneje za nove posodobitve.",
"empty_filter_description": "Nobeno obvestilo ne ustreza tem filtram.",
"filter_all": "Vse",
"filter_unread": "Neprebrano",
"filter_friends": "Prijatelji",
"filter_badges": "Značke",
"filter_upvotes": "Glasovi za všečkanje",
"filter_local": "Lokalno",
"load_more": "Naloži več",
"dismiss": "Opusti",
"accept": "Sprejmi",
"refuse": "Zavrni",
"notification": "Obvestilo",
"friend_request_received_title": "Nova prijateljska zahteva!",
"friend_request_received_description": "{{displayName}} želi biti vaš prijatelj",
"friend_request_accepted_title": "Zahteva za prijateljstvo sprejeta!",
"friend_request_accepted_description": "{{displayName}} je sprejel vašo zahtevo",
"badge_received_title": "Prejeli ste novo značko!",
"badge_received_description": "{{badgeName}}",
"review_upvote_title": "Vaša recenzija za {{gameTitle}} je dobila glasove!",
"review_upvote_description": "Vaša recenzija je dobila {{count}} novih glasov",
"marked_all_as_read": "Vsa obvestila označena kot prebrana",
"failed_to_mark_as_read": "Neuspešno označevanje obvestil kot prebranih",
"cleared_all": "Vsa obvestila izbrisana",
"failed_to_clear": "Neuspešno brisanje obvestil",
"failed_to_load": "Neuspešno nalaganje obvestil",
"failed_to_dismiss": "Neuspešno opustitev obvestila",
"friend_request_accepted": "Zahteva za prijateljstvo sprejeta",
"friend_request_refused": "Zahteva za prijateljstvo zavrnjena"
}
}
}

View File

@@ -16,7 +16,6 @@
"downloads": "İndirilenler", "downloads": "İndirilenler",
"settings": "Ayarlar", "settings": "Ayarlar",
"my_library": "Kütüphanem", "my_library": "Kütüphanem",
"library": "Kütüphane",
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)", "downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
"paused": "{{title}} (Duraklatıldı)", "paused": "{{title}} (Duraklatıldı)",
"downloading": "{{title}} (%{{percentage}} - İndiriliyor…)", "downloading": "{{title}} (%{{percentage}} - İndiriliyor…)",
@@ -27,69 +26,7 @@
"sign_in": "Giriş Yap", "sign_in": "Giriş Yap",
"friends": "Arkadaşlar", "friends": "Arkadaşlar",
"need_help": "Yardıma mı ihtiyacınız var?", "need_help": "Yardıma mı ihtiyacınız var?",
"favorites": "Favoriler", "favorites": "Favoriler"
"playable_button_title": "Şu anda oynayabileceğin oyunları göster",
"add_custom_game_tooltip": "Özel Oyun Ekle",
"show_playable_only_tooltip": "Sadece Oynanabilirleri Göster",
"custom_game_modal": "Özel Oyun Ekle",
"custom_game_modal_description": "Çalıştırılabilir bir dosya seçerek kütüphanene özel oyun ekle",
"custom_game_modal_executable_path": "Çalıştırılabilir Dosya Yolu",
"custom_game_modal_select_executable": "Çalıştırılabilir dosya seç",
"custom_game_modal_title": "Başlık",
"custom_game_modal_enter_title": "Başlık gir",
"custom_game_modal_browse": "Gözat",
"custom_game_modal_cancel": "İptal",
"custom_game_modal_add": "Oyun Ekle",
"custom_game_modal_adding": "Oyun Ekleniyor...",
"custom_game_modal_success": "Özel oyun başarıyla eklendi",
"custom_game_modal_failed": "Özel oyun eklenemedi",
"custom_game_modal_executable": "Çalıştırılabilir",
"edit_game_modal": "Varlıkları Özelleştir",
"edit_game_modal_description": "Oyun varlıklarını ve detaylarını özelleştir",
"edit_game_modal_title": "Başlık",
"edit_game_modal_enter_title": "Başlık gir",
"edit_game_modal_image": "Görsel",
"edit_game_modal_select_image": "Görsel seç",
"edit_game_modal_browse": "Gözat",
"edit_game_modal_image_preview": "Görsel önizleme",
"edit_game_modal_icon": "İkon",
"edit_game_modal_select_icon": "İkon seç",
"edit_game_modal_icon_preview": "İkon önizleme",
"edit_game_modal_logo": "Logo",
"edit_game_modal_select_logo": "Logo seç",
"edit_game_modal_logo_preview": "Logo önizleme",
"edit_game_modal_hero": "Kütüphane Hero",
"edit_game_modal_select_hero": "Kütüphane hero görseli seç",
"edit_game_modal_hero_preview": "Kütüphane hero görseli önizleme",
"edit_game_modal_cancel": "İptal et",
"edit_game_modal_update": "Güncelle",
"edit_game_modal_updating": "Güncelleniyor...",
"edit_game_modal_fill_required": "Lütfen tüm gerekli alanları doldur",
"edit_game_modal_success": "Varlıklar başarıyla güncellendi",
"edit_game_modal_failed": "Varlıklar güncellenemedi",
"edit_game_modal_image_filter": "Görsel",
"edit_game_modal_icon_resolution": "Önerilen çözünürlük: 256x256px",
"edit_game_modal_logo_resolution": "Önerilen çözünürlük: 640x360px",
"edit_game_modal_hero_resolution": "Önerilen çözünürlük: 1920x620px",
"edit_game_modal_assets": "Varlıklar",
"edit_game_modal_drop_icon_image_here": "İkon görselini buraya bırak",
"edit_game_modal_drop_logo_image_here": "Logo görselini buraya bırak",
"edit_game_modal_drop_hero_image_here": "Hero görselini buraya bırak",
"edit_game_modal_drop_to_replace_icon": "İkonu değiştirmek için buraya bırak",
"edit_game_modal_drop_to_replace_logo": "Logoyu değiştirmek için buraya bırak",
"edit_game_modal_drop_to_replace_hero": "Hero'yu değiştirmek için buraya bırak",
"install_decky_plugin": "Decky Plugin Kur",
"update_decky_plugin": "Decky Plugin Güncelle",
"decky_plugin_installed_version": "Decky Plugin (v{{version}})",
"install_decky_plugin_title": "Hydra Decky Plugin Kur",
"install_decky_plugin_message": "Bu işlem Decky Loader için Hydra plugin'ini indirecek ve kuracak. Bu işlem yükseltilmiş izinler gerektirebilir. Devam et?",
"update_decky_plugin_title": "Hydra Decky Plugin Güncelle",
"update_decky_plugin_message": "Hydra Decky plugin'inin yeni bir sürümü mevcut. Şimdi güncellemek ister misin?",
"decky_plugin_installed": "Decky plugin v{{version}} başarıyla kuruldu",
"decky_plugin_installation_failed": "Decky plugin kurulamadı: {{error}}",
"decky_plugin_installation_error": "Decky plugin kurulumu hatası: {{error}}",
"confirm": "Onayla",
"cancel": "İptal"
}, },
"header": { "header": {
"search": "Oyunlarda Ara", "search": "Oyunlarda Ara",
@@ -98,8 +35,6 @@
"downloads": "İndirilenler", "downloads": "İndirilenler",
"search_results": "Arama Sonuçları", "search_results": "Arama Sonuçları",
"settings": "Ayarlar", "settings": "Ayarlar",
"search_library": "Kütüphanede ara",
"library": "Kütüphane",
"version_available_install": "{{version}} sürümü mevcut. Yeniden başlatıp yüklemek için tıklayın.", "version_available_install": "{{version}} sürümü mevcut. Yeniden başlatıp yüklemek için tıklayın.",
"version_available_download": "{{version}} sürümü mevcut. İndirmek için tıklayın." "version_available_download": "{{version}} sürümü mevcut. İndirmek için tıklayın."
}, },
@@ -268,108 +203,7 @@
"create_start_menu_shortcut": "Başlat Menüsüne kısayol oluştur", "create_start_menu_shortcut": "Başlat Menüsüne kısayol oluştur",
"invalid_wine_prefix_path": "Geçersiz Wine ön ek yolu", "invalid_wine_prefix_path": "Geçersiz Wine ön ek yolu",
"invalid_wine_prefix_path_description": "Wine ön ek yolu hatalı. Lütfen yolu kontrol edin ve tekrar deneyin.", "invalid_wine_prefix_path_description": "Wine ön ek yolu hatalı. Lütfen yolu kontrol edin ve tekrar deneyin.",
"missing_wine_prefix": "Linux'ta yedekleme oluşturmak için Wine ön eki gereklidir", "missing_wine_prefix": "Linux'ta yedekleme oluşturmak için Wine ön eki gereklidir"
"already_in_library": "Zaten kütüphanede",
"create_shortcut_simple": "Kısayol oluştur",
"properties": "Özellikler",
"new_download_option": "Yeni",
"add_to_favorites": "Favorilere ekle",
"remove_from_favorites": "Favorilerden çıkar",
"failed_update_favorites": "Favoriler güncellenemedi",
"game_removed_from_library": "Oyun kütüphaneden çıkarıldı",
"failed_remove_from_library": "Kütüphaneden çıkarılamadı",
"files_removed_success": "Dosyalar başarıyla kaldırıldı",
"failed_remove_files": "Dosyalar kaldırılamadı",
"rating_count": "Puan",
"show_more": "Daha fazla göster",
"show_less": "Daha az göster",
"reviews": "İncelemeler",
"review_played_for": "Oynama süresi",
"leave_a_review": "İnceleme Yap",
"write_review_placeholder": "Bu oyun hakkındaki düşüncelerini paylaş...",
"sort_newest": "En yeni",
"no_reviews_yet": "Henüz inceleme yok",
"be_first_to_review": "Bu oyun hakkındaki düşüncelerini paylaşan ilk kişi ol!",
"sort_oldest": "En eski",
"sort_highest_score": "En yüksek puan",
"sort_lowest_score": "En düşük puan",
"sort_most_voted": "En çok oy",
"rating": "Puan",
"rating_stats": "Puan",
"rating_very_negative": "Çok Olumsuz",
"rating_negative": "Olumsuz",
"rating_neutral": "Nötr",
"rating_positive": "Olumlu",
"rating_very_positive": "Çok Olumlu",
"submit_review": "Gönder",
"submitting": "Gönderiliyor...",
"review_submitted_successfully": "İnceleme başarıyla gönderildi!",
"review_submission_failed": "İnceleme gönderilemedi. Lütfen tekrar dene.",
"review_cannot_be_empty": "İnceleme metin alanı boş olamaz.",
"review_deleted_successfully": "İnceleme başarıyla silindi.",
"review_deletion_failed": "İnceleme silinemedi. Lütfen tekrar dene.",
"loading_reviews": "İncelemeler yükleniyor...",
"loading_more_reviews": "Daha fazla inceleme yükleniyor...",
"load_more_reviews": "Daha fazla inceleme yükle",
"you_seemed_to_enjoy_this_game": "Bu oyunu beğenmiş görünüyorsun",
"would_you_recommend_this_game": "Bu oyun hakkında bir inceleme yazmak ister misin?",
"yes": "Evet",
"maybe_later": "Belki sonra",
"backup_failed": "Yedekleme başarısız",
"update_playtime_title": "Oynama süresini güncelle",
"update_playtime_description": "{{game}} için oynama süresini manuel olarak güncelle",
"update_playtime": "Oynama süresini güncelle",
"update_playtime_success": "Oynama süresi başarıyla güncellendi",
"update_playtime_error": "Oynama süresi güncellenemedi",
"update_game_playtime": "Oyun oynama süresini güncelle",
"manual_playtime_warning": "Saatlerin manuel olarak güncellendiği işaretlenecek ve bu geri alınamaz.",
"manual_playtime_tooltip": "Bu oynama süresi manuel olarak güncellendi",
"game_removed_from_pinned": "Oyun sabitlenmişlerden çıkarıldı",
"game_added_to_pinned": "Oyun sabitlenmişlere eklendi",
"artifact_renamed": "Yedekleme başarıyla yeniden adlandırıldı",
"rename_artifact": "Yedeklemeyi Yeniden Adlandır",
"rename_artifact_description": "Yedeklemeyi daha açıklayıcı bir isimle yeniden adlandır",
"artifact_name_label": "Yedekleme adı",
"artifact_name_placeholder": "Yedekleme için bir isim gir",
"save_changes": "Değişiklikleri kaydet",
"required_field": "Bu alan gereklidir",
"max_length_field": "Bu alan {{length}} karakterden az olmalıdır",
"freeze_backup": "Otomatik yedeklemeler tarafından üzerine yazılmasın diye sabitle",
"unfreeze_backup": "Sabitlemeyi kaldır",
"backup_frozen": "Yedekleme sabitlendi",
"backup_unfrozen": "Yedekleme sabitlemesi kaldırıldı",
"backup_freeze_failed": "Yedekleme sabitlenemedi",
"backup_freeze_failed_description": "Otomatik yedeklemeler için en az bir boş alan bırakmalısın",
"edit_game_modal_button": "Oyun varlıklarını özelleştir",
"game_details": "Oyun Detayları",
"currency_symbol": "₺",
"currency_country": "tr",
"prices": "Fiyatlar",
"no_prices_found": "Fiyat bulunamadı",
"view_all_prices": "Tüm fiyatları görüntülemek için tıkla",
"retail_price": "Perakende fiyatı",
"keyshop_price": "Anahtar dükkanı fiyatı",
"historical_retail": "Geçmiş perakende",
"historical_keyshop": "Geçmiş anahtar dükkanı",
"language": "Dil",
"caption": "Altyazı",
"audio": "Ses",
"filter_by_source": "Kaynağa göre filtrele",
"no_repacks_found": "Bu oyun için kaynak bulunamadı",
"delete_review": "İncelemeyi sil",
"remove_review": "İncelemeyi Kaldır",
"delete_review_modal_title": "İncelemeni silmek istediğinden emin misin?",
"delete_review_modal_description": "Bu işlem geri alınamaz.",
"delete_review_modal_delete_button": "Sil",
"delete_review_modal_cancel_button": "İptal",
"vote_failed": "Oyun kaydı başarısız oldu. Lütfen tekrar dene.",
"show_original": "Orijinali göster",
"show_translation": "Çeviriyi göster",
"show_original_translated_from": "Orijinali göster ({{language}} dilinden çevrilmiştir)",
"hide_original": "Orijinali gizle",
"review_from_blocked_user": "Engellenen kullanıcıdan gelen inceleme",
"show": "Göster",
"hide": "Gizle"
}, },
"activation": { "activation": {
"title": "Hydra'yı Etkinleştir", "title": "Hydra'yı Etkinleştir",
@@ -545,33 +379,7 @@
"hidden": "Gizli", "hidden": "Gizli",
"test_notification": "Test bildirimi", "test_notification": "Test bildirimi",
"notification_preview": "Başarı Bildirimi Önizlemesi", "notification_preview": "Başarı Bildirimi Önizlemesi",
"enable_friend_start_game_notifications": "Bir arkadaşınız oyun oynamaya başladığında", "enable_friend_start_game_notifications": "Bir arkadaşınız oyun oynamaya başladığında"
"adding": "Ekleniyor…",
"failed_add_download_source": "İndirme kaynağı eklenemedi. Lütfen tekrar dene.",
"download_source_already_exists": "Bu indirme kaynağı URL'si zaten mevcut.",
"download_source_pending_matching": "Yakında güncellenecek",
"download_source_matched": "Güncel",
"download_source_matching": "Güncelleniyor",
"download_source_failed": "Hata",
"download_source_no_information": "Bilgi mevcut değil",
"removed_all_download_sources": "Tüm indirme kaynakları kaldırıldı",
"download_sources_synced_successfully": "Tüm indirme kaynakları senkronize edildi",
"importing": "İçe aktarılıyor...",
"hydra_cloud": "Hydra Cloud",
"debrid": "Debrid",
"debrid_description": "Debrid servisleri, internet hızınızla sınırlı, çeşitli dosya barındırma hizmetlerinde barındırılan dosyaları hızla indirmenize olanak tanıyan premium sınırsız indiricilerdir.",
"enable_steam_achievements": "Steam başarımları aramasını etkinleştir",
"achievement_sound_volume": "Başarım ses seviyesi",
"select_achievement_sound": "Başarım sesi seç",
"change_achievement_sound": "Başarım sesini değiştir",
"remove_achievement_sound": "Başarım sesini kaldır",
"preview_sound": "Sesi önizle",
"select": "Seç",
"preview": "Önizle",
"remove": "Kaldır",
"no_sound_file_selected": "Ses dosyası seçilmedi",
"autoplay_trailers_on_game_page": "Oyun sayfasında fragmanları otomatik olarak oynat",
"hide_to_tray_on_game_start": "Oyun başlatıldığında Hydra'yı sistem tepsisine gizle"
}, },
"notifications": { "notifications": {
"download_complete": "İndirme tamamlandı", "download_complete": "İndirme tamamlandı",
@@ -598,8 +406,7 @@
"game_card": { "game_card": {
"available_one": "Mevcut", "available_one": "Mevcut",
"available_other": "Mevcut", "available_other": "Mevcut",
"no_downloads": "İndirme mevcut değil", "no_downloads": "İndirme mevcut değil"
"calculating": "Hesaplanıyor"
}, },
"binary_not_found_modal": { "binary_not_found_modal": {
"title": "Programlar Yüklü Değil", "title": "Programlar Yüklü Değil",
@@ -691,45 +498,7 @@
"achievements_unlocked": "Açılan başarımlar", "achievements_unlocked": "Açılan başarımlar",
"earned_points": "Kazanılan puanlar", "earned_points": "Kazanılan puanlar",
"show_achievements_on_profile": "Başarımlarını profilinde göster", "show_achievements_on_profile": "Başarımlarını profilinde göster",
"show_points_on_profile": "Kazanılan puanlarını profilinde göster", "show_points_on_profile": "Kazanılan puanlarını profilinde göster"
"amount_hours_short": "{{amount}}s",
"amount_minutes_short": "{{amount}}d",
"pinned": "Sabitlenmiş",
"sort_by": "Sırala:",
"achievements_earned": "Kazanılan başarımlar",
"played_recently": "Son oynanan",
"playtime": "Oynama süresi",
"manual_playtime_tooltip": "Bu oynama süresi manuel olarak güncellendi",
"error_adding_friend": "Arkadaş isteği gönderilemedi. Lütfen arkadaş kodunu kontrol et",
"friend_code_length_error": "Arkadaş kodu 8 karakter olmalıdır",
"game_removed_from_pinned": "Oyun sabitlenmişlerden çıkarıldı",
"game_added_to_pinned": "Oyun sabitlenmişlere eklendi",
"karma": "Karma",
"karma_count": "karma",
"user_reviews": "İncelemeler",
"delete_review": "İncelemeyi Sil",
"loading_reviews": "İncelemeler yükleniyor..."
},
"library": {
"library": "Kütüphane",
"play": "Oyna",
"download": "İndir",
"downloading": "İndiriliyor",
"game": "oyun",
"games": "oyunlar",
"grid_view": "Izgara görünümü",
"compact_view": "Kompakt görünüm",
"large_view": "Büyük görünüm",
"no_games_title": "Kütüphanen boş",
"no_games_description": "Başlamak için katalogdan oyun ekle veya indir",
"amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika",
"amount_hours_short": "{{amount}}s",
"amount_minutes_short": "{{amount}}d",
"manual_playtime_tooltip": "Bu oynama süresi manuel olarak güncellendi",
"all_games": "Tüm Oyunlar",
"recently_played": "Son Oynanan",
"favorites": "Favoriler"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Başarım açıldı", "achievement_unlocked": "Başarım açıldı",

View File

@@ -668,7 +668,8 @@
"game_removed_from_pinned": "Гру видалено із закріплених", "game_removed_from_pinned": "Гру видалено із закріплених",
"game_added_to_pinned": "Гру додано до закріплених", "game_added_to_pinned": "Гру додано до закріплених",
"karma": "Карма", "karma": "Карма",
"karma_count": "карма" "karma_count": "карма",
"karma_description": "Зароблена позитивними оцінками на відгуках"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Досягнення розблоковано", "achievement_unlocked": "Досягнення розблоковано",

View File

@@ -27,68 +27,7 @@
"friends": "好友", "friends": "好友",
"favorites": "收藏", "favorites": "收藏",
"need_help": "需要帮助?", "need_help": "需要帮助?",
"playable_button_title": "仅显示现在可以游玩的游戏", "playable_button_title": "仅显示现在可以游玩的游戏"
"add_custom_game_tooltip": "添加自定义游戏",
"cancel": "取消",
"confirm": "确认",
"custom_game_modal": "添加自定义游戏",
"custom_game_modal_add": "添加游戏",
"custom_game_modal_adding": "正在添加游戏...",
"custom_game_modal_browse": "浏览",
"custom_game_modal_cancel": "取消",
"custom_game_modal_description": "通过选择可执行文件将自定义游戏添加到您的库中",
"custom_game_modal_enter_title": "输入标题",
"custom_game_modal_executable": "可执行文件",
"custom_game_modal_executable_path": "可执行文件路径",
"custom_game_modal_failed": "添加自定义游戏失败",
"custom_game_modal_select_executable": "选择可执行文件",
"custom_game_modal_success": "自定义游戏添加成功",
"custom_game_modal_title": "标题",
"decky_plugin_installation_error": "安装 Decky 插件出错: {{error}}",
"decky_plugin_installation_failed": "Decky 插件安装失败: {{error}}",
"decky_plugin_installed": "Decky 插件 v{{version}} 安装成功",
"decky_plugin_installed_version": "Decky 插件 (v{{version}})",
"edit_game_modal": "自定义资源",
"edit_game_modal_assets": "资源",
"edit_game_modal_browse": "浏览",
"edit_game_modal_cancel": "取消",
"edit_game_modal_description": "自定义游戏资源和详情",
"edit_game_modal_drop_hero_image_here": "拖放主图像到此处",
"edit_game_modal_drop_icon_image_here": "拖放图标到此处",
"edit_game_modal_drop_logo_image_here": "拖放Logo到此处",
"edit_game_modal_drop_to_replace_hero": "拖放以替换主图像",
"edit_game_modal_drop_to_replace_icon": "拖放以替换图标",
"edit_game_modal_drop_to_replace_logo": "拖放以替换Logo",
"edit_game_modal_enter_title": "输入标题",
"edit_game_modal_failed": "资源更新失败",
"edit_game_modal_fill_required": "请填写所有必填项",
"edit_game_modal_hero": "库主图",
"edit_game_modal_hero_preview": "库主图预览",
"edit_game_modal_hero_resolution": "推荐分辨率: 1920x620px",
"edit_game_modal_icon": "图标",
"edit_game_modal_icon_preview": "图标预览",
"edit_game_modal_icon_resolution": "推荐分辨率: 256x256px",
"edit_game_modal_image": "图片",
"edit_game_modal_image_filter": "图片",
"edit_game_modal_image_preview": "图片预览",
"edit_game_modal_logo": "Logo",
"edit_game_modal_logo_preview": "Logo预览",
"edit_game_modal_logo_resolution": "推荐分辨率: 640x360px",
"edit_game_modal_select_hero": "选择库主图",
"edit_game_modal_select_icon": "选择图标",
"edit_game_modal_select_image": "选择图片",
"edit_game_modal_select_logo": "选择Logo",
"edit_game_modal_success": "资源更新成功",
"edit_game_modal_title": "标题",
"edit_game_modal_update": "更新",
"edit_game_modal_updating": "正在更新...",
"install_decky_plugin": "安装 Decky 插件",
"install_decky_plugin_message": "这将下载并安装 Hydra 的 Decky Loader 插件。可能需要提升权限。继续吗?",
"install_decky_plugin_title": "安装 Hydra Decky 插件",
"show_playable_only_tooltip": "仅显示可游玩",
"update_decky_plugin": "更新 Decky 插件",
"update_decky_plugin_message": "有新版本的 Hydra Decky 插件可用。现在要更新吗?",
"update_decky_plugin_title": "更新 Hydra Decky 插件"
}, },
"header": { "header": {
"search": "搜索游戏", "search": "搜索游戏",
@@ -279,93 +218,7 @@
"reset_achievements_title": "您确定吗?", "reset_achievements_title": "您确定吗?",
"save_changes": "保存更改", "save_changes": "保存更改",
"unfreeze_backup": "取消固定", "unfreeze_backup": "取消固定",
"you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改", "you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改"
"add_to_favorites": "添加到收藏",
"already_in_library": "已在游戏库中",
"audio": "音频",
"backup_failed": "备份失败",
"be_first_to_review": "成为第一个分享游戏感受的人!",
"caption": "标题",
"create_shortcut_simple": "创建快捷方式",
"currency_country": "zh",
"currency_symbol": "¥",
"delete_review": "删除评价",
"delete_review_modal_cancel_button": "取消",
"delete_review_modal_delete_button": "删除",
"delete_review_modal_description": "此操作无法撤销。",
"delete_review_modal_title": "确定要删除您的评价吗?",
"edit_game_modal_button": "自定义游戏资源",
"failed_remove_files": "文件删除失败",
"failed_remove_from_library": "移出游戏库失败",
"failed_update_favorites": "收藏更新失败",
"files_removed_success": "文件已成功删除",
"filter_by_source": "按来源筛选",
"game_added_to_pinned": "游戏已添加到置顶",
"game_details": "游戏详情",
"game_removed_from_library": "游戏已从库中移除",
"game_removed_from_pinned": "游戏已从置顶移除",
"hide": "隐藏",
"hide_original": "隐藏原文",
"historical_keyshop": "历史密钥商店",
"historical_retail": "历史零售",
"keyshop_price": "密钥商店价格",
"language": "语言",
"leave_a_review": "留下评价",
"load_more_reviews": "加载更多评价",
"loading_more_reviews": "正在加载更多评价...",
"loading_reviews": "正在加载评价...",
"manual_playtime_tooltip": "该游戏时长已手动更新",
"manual_playtime_warning": "您的游戏时长将被标记为手动更新,且无法撤销。",
"maybe_later": "以后再说",
"no_prices_found": "未找到价格信息",
"no_repacks_found": "未找到该游戏的下载来源",
"no_reviews_yet": "暂无评价",
"prices": "价格",
"properties": "属性",
"rating": "评分",
"rating_count": "评分数",
"rating_negative": "差评",
"rating_neutral": "中性",
"rating_positive": "好评",
"rating_stats": "评分统计",
"rating_very_negative": "极差",
"rating_very_positive": "极好",
"remove_from_favorites": "移出收藏",
"remove_review": "移除评价",
"retail_price": "零售价格",
"review_cannot_be_empty": "评价内容不能为空。",
"review_deleted_successfully": "评价已成功删除。",
"review_deletion_failed": "评价删除失败,请重试。",
"review_from_blocked_user": "来自被屏蔽用户的评价",
"review_played_for": "已游玩",
"review_submission_failed": "评价提交失败,请重试。",
"review_submitted_successfully": "评价提交成功!",
"reviews": "评价",
"show": "显示",
"show_less": "收起",
"show_more": "展开",
"show_original": "显示原文",
"show_original_translated_from": "显示原文(由{{language}}翻译)",
"show_translation": "显示翻译",
"sort_highest_score": "最高分",
"sort_lowest_score": "最低分",
"sort_most_voted": "最多投票",
"sort_newest": "最新",
"sort_oldest": "最旧",
"submit_review": "提交",
"submitting": "正在提交...",
"update_game_playtime": "更新游戏时长",
"update_playtime": "更新时长",
"update_playtime_description": "手动更新 {{game}} 的游玩时长",
"update_playtime_error": "游戏时长更新失败",
"update_playtime_success": "游戏时长已成功更新",
"update_playtime_title": "更新游戏时长",
"view_all_prices": "点击查看所有价格",
"vote_failed": "投票失败,请重试。",
"would_you_recommend_this_game": "您想为此游戏留下评价吗?",
"write_review_placeholder": "分享您对本游戏的看法...",
"yes": "是",
"you_seemed_to_enjoy_this_game": "您似乎很喜欢这款游戏"
}, },
"activation": { "activation": {
"title": "激活 Hydra", "title": "激活 Hydra",
@@ -541,24 +394,7 @@
"update_email": "更新邮箱", "update_email": "更新邮箱",
"update_password": "更新密码", "update_password": "更新密码",
"variation": "变体", "variation": "变体",
"web_store": "网络商店", "web_store": "网络商店"
"adding": "添加中…",
"autoplay_trailers_on_game_page": "在游戏页面自动播放预告片",
"debrid": "Debrid下载服务",
"debrid_description": "Debrid服务是一种高级不限速下载器可让您以最快的网速下载托管在各类网盘上的文件仅受您的网络速度限制。",
"download_source_already_exists": "该下载源URL已存在。",
"download_source_failed": "出错",
"download_source_matched": "已更新",
"download_source_matching": "正在更新",
"download_source_no_information": "暂无信息",
"download_source_pending_matching": "即将更新",
"download_sources_synced_successfully": "所有下载源已同步",
"enable_steam_achievements": "启用Steam成就搜索",
"failed_add_download_source": "添加下载源失败,请重试。",
"hide_to_tray_on_game_start": "启动游戏时隐藏到托盘",
"hydra_cloud": "Hydra Cloud",
"importing": "导入中…",
"removed_all_download_sources": "已移除所有下载源"
}, },
"notifications": { "notifications": {
"download_complete": "下载完成", "download_complete": "下载完成",
@@ -585,8 +421,7 @@
"game_card": { "game_card": {
"no_downloads": "无可用下载选项", "no_downloads": "无可用下载选项",
"available_one": "可用", "available_one": "可用",
"available_other": "可用", "available_other": "可用"
"calculating": "正在计算"
}, },
"binary_not_found_modal": { "binary_not_found_modal": {
"title": "程序未安装", "title": "程序未安装",
@@ -680,22 +515,7 @@
"show_achievements_on_profile": "在您的个人资料上显示成就", "show_achievements_on_profile": "在您的个人资料上显示成就",
"show_points_on_profile": "在您的个人资料上显示获得的积分", "show_points_on_profile": "在您的个人资料上显示获得的积分",
"stats": "统计", "stats": "统计",
"top_percentile": "前 {{percentile}}%", "top_percentile": "前 {{percentile}}%"
"achievements_earned": "已获得成就",
"amount_hours_short": "{{amount}}小时",
"amount_minutes_short": "{{amount}}分钟",
"delete_review": "删除评价",
"game_added_to_pinned": "游戏已添加到置顶",
"game_removed_from_pinned": "游戏已从置顶移除",
"karma": "业力",
"karma_count": "业力值",
"loading_reviews": "正在加载评价...",
"manual_playtime_tooltip": "该游戏时长已手动更新",
"pinned": "已置顶",
"played_recently": "最近游玩",
"playtime": "游戏时长",
"sort_by": "排序方式:",
"user_reviews": "用户评价"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "成就已解锁", "achievement_unlocked": "成就已解锁",

View File

@@ -41,12 +41,8 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets"); export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
export const THEMES_PATH = path.join(SystemPath.getPath("userData"), "themes");
export const MAIN_LOOP_INTERVAL = 2000; export const MAIN_LOOP_INTERVAL = 2000;
export const DEFAULT_ACHIEVEMENT_SOUND_VOLUME = 0.15;
export const DECKY_PLUGINS_LOCATION = path.join( export const DECKY_PLUGINS_LOCATION = path.join(
SystemPath.getPath("home"), SystemPath.getPath("home"),
"homebrew", "homebrew",

View File

@@ -1,3 +0,0 @@
import "./get-session-hash";
import "./open-auth-window";
import "./sign-out";

View File

@@ -1,2 +0,0 @@
import "./check-for-updates";
import "./restart-and-install-update";

View File

@@ -1,4 +0,0 @@
import "./get-game-assets";
import "./get-game-shop-details";
import "./get-game-stats";
import "./get-random-game";

View File

@@ -1,4 +0,0 @@
import "./download-game-artifact";
import "./get-game-backup-preview";
import "./select-game-backup-path";
import "./upload-save-game";

View File

@@ -1,13 +0,0 @@
import { getDownloadSourcesCheckBaseline } from "@main/level";
import { registerEvent } from "../register-event";
const getDownloadSourcesCheckBaselineHandler = async (
_event: Electron.IpcMainInvokeEvent
) => {
return await getDownloadSourcesCheckBaseline();
};
registerEvent(
"getDownloadSourcesCheckBaseline",
getDownloadSourcesCheckBaselineHandler
);

View File

@@ -1,13 +0,0 @@
import { getDownloadSourcesSinceValue } from "@main/level";
import { registerEvent } from "../register-event";
const getDownloadSourcesSinceValueHandler = async (
_event: Electron.IpcMainInvokeEvent
) => {
return await getDownloadSourcesSinceValue();
};
registerEvent(
"getDownloadSourcesSinceValue",
getDownloadSourcesSinceValueHandler
);

View File

@@ -1,6 +0,0 @@
import "./add-download-source";
import "./get-download-sources-check-baseline";
import "./get-download-sources-since-value";
import "./get-download-sources";
import "./remove-download-source";
import "./sync-download-sources";

View File

@@ -1,2 +0,0 @@
import "./check-folder-write-permission";
import "./get-disk-free-space";

View File

@@ -1,22 +1,98 @@
import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants"; import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import "./auth"; import "./catalogue/get-game-shop-details";
import "./autoupdater"; import "./catalogue/get-random-game";
import "./catalogue"; import "./catalogue/get-game-stats";
import "./cloud-save"; import "./hardware/get-disk-free-space";
import "./download-sources"; import "./hardware/check-folder-write-permission";
import "./hardware"; import "./library/add-game-to-library";
import "./library"; import "./library/add-custom-game-to-library";
import "./leveldb"; import "./library/update-custom-game";
import "./misc"; import "./library/update-game-custom-assets";
import "./notifications"; import "./library/add-game-to-favorites";
import "./profile"; import "./library/remove-game-from-favorites";
import "./themes"; import "./library/toggle-game-pin";
import "./torrenting"; import "./library/create-game-shortcut";
import "./user"; import "./library/close-game";
import "./user-preferences"; import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/get-library";
import "./library/extract-game-download";
import "./library/open-game";
import "./library/open-game-executable-path";
import "./library/open-game-installer";
import "./library/open-game-installer-path";
import "./library/update-executable-path";
import "./library/update-launch-options";
import "./library/verify-executable-path";
import "./library/remove-game";
import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./library/change-game-playtime";
import "./library/toggle-automatic-cloud-sync";
import "./library/get-default-wine-prefix-selection-path";
import "./library/cleanup-unused-assets";
import "./library/create-steam-shortcut";
import "./library/copy-custom-game-asset";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";
import "./misc/show-item-in-folder";
import "./misc/install-common-redist";
import "./misc/can-install-common-redist";
import "./misc/save-temp-file";
import "./misc/delete-temp-file";
import "./misc/install-hydra-decky-plugin";
import "./misc/get-hydra-decky-plugin-info";
import "./misc/check-homebrew-folder-exists";
import "./misc/hydra-api-call";
import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
import "./torrenting/start-game-download";
import "./torrenting/pause-game-seed";
import "./torrenting/resume-game-seed";
import "./torrenting/check-debrid-availability";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid";
import "./user-preferences/authenticate-torbox";
import "./download-sources/add-download-source";
import "./download-sources/sync-download-sources";
import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
import "./user/get-auth";
import "./user/get-unlocked-achievements";
import "./user/get-compared-unlocked-achievements";
import "./profile/get-me";
import "./profile/update-profile";
import "./profile/process-profile-image";
import "./profile/sync-friend-requests";
import "./cloud-save/download-game-artifact";
import "./cloud-save/get-game-backup-preview";
import "./cloud-save/upload-save-game";
import "./cloud-save/select-game-backup-path";
import "./notifications/publish-new-repacks-notification";
import "./notifications/update-achievement-notification-window";
import "./notifications/show-achievement-test-notification";
import "./themes/add-custom-theme";
import "./themes/delete-custom-theme";
import "./themes/get-all-custom-themes";
import "./themes/delete-all-custom-themes";
import "./themes/update-custom-theme";
import "./themes/open-editor-window";
import "./themes/get-custom-theme-by-id";
import "./themes/get-active-custom-theme";
import "./themes/close-editor-window";
import "./themes/toggle-custom-theme";
import "./download-sources/remove-download-source";
import "./download-sources/get-download-sources";
import { isPortableVersion } from "@main/helpers"; import { isPortableVersion } from "@main/helpers";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");

View File

@@ -1,27 +0,0 @@
import { db } from "@main/level";
const sublevelCache = new Map<
string,
ReturnType<typeof db.sublevel<string, unknown>>
>();
/**
* Gets a sublevel by name, creating it if it doesn't exist.
* All sublevels use "json" encoding by default.
* @param sublevelName - The name of the sublevel to get or create
* @returns The sublevel instance
*/
export const getSublevelByName = (
sublevelName: string
): ReturnType<typeof db.sublevel<string, unknown>> => {
if (sublevelCache.has(sublevelName)) {
return sublevelCache.get(sublevelName)!;
}
// All sublevels use "json" encoding - this cannot be changed per sublevel
const sublevel = db.sublevel<string, unknown>(sublevelName, {
valueEncoding: "json",
});
sublevelCache.set(sublevelName, sublevel);
return sublevel;
};

View File

@@ -1,6 +0,0 @@
import "./leveldb-get";
import "./leveldb-put";
import "./leveldb-del";
import "./leveldb-clear";
import "./leveldb-values";
import "./leveldb-iterator";

View File

@@ -1,18 +0,0 @@
import { registerEvent } from "../register-event";
import { getSublevelByName } from "./helpers";
import { logger } from "@main/services";
const leveldbClear = async (
_event: Electron.IpcMainInvokeEvent,
sublevelName: string
) => {
try {
const sublevel = getSublevelByName(sublevelName);
await sublevel.clear();
} catch (error) {
logger.error("Error in leveldbClear", error);
throw error;
}
};
registerEvent("leveldbClear", leveldbClear);

View File

@@ -1,28 +0,0 @@
import { registerEvent } from "../register-event";
import { db } from "@main/level";
import { getSublevelByName } from "./helpers";
import { logger } from "@main/services";
const leveldbDel = async (
_event: Electron.IpcMainInvokeEvent,
key: string,
sublevelName?: string | null
) => {
try {
if (sublevelName) {
const sublevel = getSublevelByName(sublevelName);
await sublevel.del(key);
} else {
await db.del(key);
}
} catch (error) {
if (error instanceof Error && error.name === "NotFoundError") {
// NotFoundError on delete is not an error, just return
return;
}
logger.error("Error in leveldbDel", error);
throw error;
}
};
registerEvent("leveldbDel", leveldbDel);

View File

@@ -1,28 +0,0 @@
import { registerEvent } from "../register-event";
import { db } from "@main/level";
import { getSublevelByName } from "./helpers";
import { logger } from "@main/services";
const leveldbGet = async (
_event: Electron.IpcMainInvokeEvent,
key: string,
sublevelName?: string | null,
valueEncoding: "json" | "utf8" = "json"
) => {
try {
if (sublevelName) {
// Note: sublevels always use "json" encoding, valueEncoding parameter is ignored
const sublevel = getSublevelByName(sublevelName);
return sublevel.get(key);
}
return db.get<string, unknown>(key, { valueEncoding });
} catch (error) {
if (error instanceof Error && error.name === "NotFoundError") {
return null;
}
logger.error("Error in leveldbGet", error);
throw error;
}
};
registerEvent("leveldbGet", leveldbGet);

View File

@@ -1,18 +0,0 @@
import { registerEvent } from "../register-event";
import { getSublevelByName } from "./helpers";
import { logger } from "@main/services";
const leveldbIterator = async (
_event: Electron.IpcMainInvokeEvent,
sublevelName: string
) => {
try {
const sublevel = getSublevelByName(sublevelName);
return sublevel.iterator().all();
} catch (error) {
logger.error("Error in leveldbIterator", error);
throw error;
}
};
registerEvent("leveldbIterator", leveldbIterator);

View File

@@ -1,27 +0,0 @@
import { registerEvent } from "../register-event";
import { db } from "@main/level";
import { getSublevelByName } from "./helpers";
import { logger } from "@main/services";
const leveldbPut = async (
_event: Electron.IpcMainInvokeEvent,
key: string,
value: unknown,
sublevelName?: string | null,
valueEncoding: "json" | "utf8" = "json"
) => {
try {
if (sublevelName) {
// Note: sublevels always use "json" encoding, valueEncoding parameter is ignored
const sublevel = getSublevelByName(sublevelName);
await sublevel.put(key, value);
} else {
await db.put<string, unknown>(key, value, { valueEncoding });
}
} catch (error) {
logger.error("Error in leveldbPut", error);
throw error;
}
};
registerEvent("leveldbPut", leveldbPut);

View File

@@ -1,18 +0,0 @@
import { registerEvent } from "../register-event";
import { getSublevelByName } from "./helpers";
import { logger } from "@main/services";
const leveldbValues = async (
_event: Electron.IpcMainInvokeEvent,
sublevelName: string
) => {
try {
const sublevel = getSublevelByName(sublevelName);
return sublevel.values().all();
} catch (error) {
logger.error("Error in leveldbValues", error);
throw error;
}
};
registerEvent("leveldbValues", leveldbValues);

View File

@@ -1,27 +0,0 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { logger } from "@main/services";
import type { GameShop } from "@types";
const clearNewDownloadOptions = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
try {
await gamesSublevel.put(gameKey, {
...game,
newDownloadOptionsCount: undefined,
});
logger.info(`Cleared newDownloadOptionsCount for game ${gameKey}`);
} catch (error) {
logger.error(`Failed to clear newDownloadOptionsCount: ${error}`);
}
};
registerEvent("clearNewDownloadOptions", clearNewDownloadOptions);

View File

@@ -1,23 +0,0 @@
import fs from "node:fs";
import { registerEvent } from "../register-event";
import { logger } from "@main/services";
const deleteArchive = async (
_event: Electron.IpcMainInvokeEvent,
filePath: string
) => {
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
logger.info(`Deleted archive: ${filePath}`);
return true;
}
return true;
} catch (err) {
logger.error(`Failed to delete archive: ${filePath}`, err);
return false;
}
};
registerEvent("deleteArchive", deleteArchive);

View File

@@ -15,7 +15,14 @@ const deleteGameFolder = async (
const downloadKey = levelKeys.game(shop, objectId); const downloadKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(downloadKey); const download = await downloadsSublevel.get(downloadKey);
if (!download) return; if (!download?.folderName) return;
const folderPath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
const metaPath = `${folderPath}.meta`;
const deleteFile = async (filePath: string, isDirectory = false) => { const deleteFile = async (filePath: string, isDirectory = false) => {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
@@ -40,18 +47,8 @@ const deleteGameFolder = async (
} }
}; };
if (download.folderName) { await deleteFile(folderPath, true);
const folderPath = path.join( await deleteFile(metaPath);
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
const metaPath = `${folderPath}.meta`;
await deleteFile(folderPath, true);
await deleteFile(metaPath);
}
await downloadsSublevel.del(downloadKey); await downloadsSublevel.del(downloadKey);
}; };

View File

@@ -22,7 +22,6 @@ const extractGameDownload = async (
await downloadsSublevel.put(gameKey, { await downloadsSublevel.put(gameKey, {
...download, ...download,
extracting: true, extracting: true,
extractionProgress: 0,
}); });
const gameFilesManager = new GameFilesManager(shop, objectId); const gameFilesManager = new GameFilesManager(shop, objectId);

View File

@@ -1,58 +0,0 @@
import path from "node:path";
import fs from "node:fs";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { registerEvent } from "../register-event";
import { downloadsSublevel, levelKeys } from "@main/level";
import { GameShop } from "@types";
const getGameInstallerActionType = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
): Promise<"install" | "open-folder"> => {
const downloadKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(downloadKey);
if (!download?.folderName) return "open-folder";
const gamePath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
if (!fs.existsSync(gamePath)) {
return "open-folder";
}
// macOS always opens folder
if (process.platform === "darwin") {
return "open-folder";
}
// If path is a file, it will show in folder (open-folder behavior)
if (fs.lstatSync(gamePath).isFile()) {
return "open-folder";
}
// Check for setup.exe
const setupPath = path.join(gamePath, "setup.exe");
if (fs.existsSync(setupPath)) {
return "install";
}
// Check if there's exactly one .exe file
const gamePathFileNames = fs.readdirSync(gamePath);
const gamePathExecutableFiles = gamePathFileNames.filter(
(fileName: string) => path.extname(fileName).toLowerCase() === ".exe"
);
if (gamePathExecutableFiles.length === 1) {
return "install";
}
// Otherwise, opens folder
return "open-folder";
};
registerEvent("getGameInstallerActionType", getGameInstallerActionType);

View File

@@ -2,7 +2,6 @@ import type { LibraryGame } from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { import {
downloadsSublevel, downloadsSublevel,
gameAchievementsSublevel,
gamesShopAssetsSublevel, gamesShopAssetsSublevel,
gamesSublevel, gamesSublevel,
} from "@main/level"; } from "@main/level";
@@ -19,28 +18,15 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
const download = await downloadsSublevel.get(key); const download = await downloadsSublevel.get(key);
const gameAssets = await gamesShopAssetsSublevel.get(key); const gameAssets = await gamesShopAssetsSublevel.get(key);
let unlockedAchievementCount = game.unlockedAchievementCount ?? 0;
if (!game.unlockedAchievementCount) {
const achievements = await gameAchievementsSublevel.get(key);
unlockedAchievementCount =
achievements?.unlockedAchievements?.length ?? 0;
}
return { return {
id: key, id: key,
...game, ...game,
download: download ?? null, download: download ?? null,
unlockedAchievementCount,
achievementCount: game.achievementCount ?? 0,
// Spread gameAssets last to ensure all image URLs are properly set
...gameAssets, ...gameAssets,
// Preserve custom image URLs from game if they exist // Ensure compatibility with LibraryGame type
customIconUrl: game.customIconUrl, libraryHeroImageUrl:
customLogoImageUrl: game.customLogoImageUrl, game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl,
customHeroImageUrl: game.customHeroImageUrl, } as LibraryGame;
};
}) })
); );
}); });

View File

@@ -1,35 +0,0 @@
import "./add-custom-game-to-library";
import "./add-game-to-favorites";
import "./add-game-to-library";
import "./change-game-playtime";
import "./cleanup-unused-assets";
import "./clear-new-download-options";
import "./close-game";
import "./copy-custom-game-asset";
import "./create-game-shortcut";
import "./create-steam-shortcut";
import "./delete-archive";
import "./delete-game-folder";
import "./extract-game-download";
import "./get-default-wine-prefix-selection-path";
import "./get-game-by-object-id";
import "./get-game-installer-action-type";
import "./get-library";
import "./open-game-executable-path";
import "./open-game-installer-path";
import "./open-game-installer";
import "./open-game";
import "./refresh-library-assets";
import "./remove-game-from-favorites";
import "./remove-game-from-library";
import "./remove-game";
import "./reset-game-achievements";
import "./scan-installed-games";
import "./select-game-wine-prefix";
import "./toggle-automatic-cloud-sync";
import "./toggle-game-pin";
import "./update-custom-game";
import "./update-executable-path";
import "./update-game-custom-assets";
import "./update-launch-options";
import "./verify-executable-path";

View File

@@ -38,6 +38,7 @@ const openGameInstaller = async (
); );
if (!fs.existsSync(gamePath)) { if (!fs.existsSync(gamePath)) {
await downloadsSublevel.del(downloadKey);
return true; return true;
} }

View File

@@ -1,8 +0,0 @@
import { registerEvent } from "../register-event";
import { mergeWithRemoteGames } from "@main/services";
const refreshLibraryAssets = async () => {
await mergeWithRemoteGames();
};
registerEvent("refreshLibraryAssets", refreshLibraryAssets);

View File

@@ -1,143 +0,0 @@
import path from "node:path";
import fs from "node:fs";
import { t } from "i18next";
import { registerEvent } from "../register-event";
import { gamesSublevel } from "@main/level";
import {
GameExecutables,
LocalNotificationManager,
logger,
WindowManager,
} from "@main/services";
const SCAN_DIRECTORIES = [
String.raw`C:\Games`,
String.raw`D:\Games`,
String.raw`C:\Program Files (x86)\Steam\steamapps\common`,
String.raw`C:\Program Files\Steam\steamapps\common`,
String.raw`C:\Program Files (x86)\DODI-Repacks`,
];
interface FoundGame {
title: string;
executablePath: string;
}
interface ScanResult {
foundGames: FoundGame[];
total: number;
}
async function searchInDirectories(
executableNames: Set<string>
): Promise<string | null> {
for (const scanDir of SCAN_DIRECTORIES) {
if (!fs.existsSync(scanDir)) continue;
const foundPath = await findExecutableInFolder(scanDir, executableNames);
if (foundPath) return foundPath;
}
return null;
}
async function publishScanNotification(foundCount: number): Promise<void> {
const hasFoundGames = foundCount > 0;
await LocalNotificationManager.createNotification(
"SCAN_GAMES_COMPLETE",
t(
hasFoundGames
? "scan_games_complete_title"
: "scan_games_no_results_title",
{ ns: "notifications" }
),
t(
hasFoundGames
? "scan_games_complete_description"
: "scan_games_no_results_description",
{ ns: "notifications", count: foundCount }
),
{ url: "/library?openScanModal=true" }
);
}
const scanInstalledGames = async (
_event: Electron.IpcMainInvokeEvent
): Promise<ScanResult> => {
const games = await gamesSublevel
.iterator()
.all()
.then((results) =>
results
.filter(
([_key, game]) => game.isDeleted === false && game.shop !== "custom"
)
.map(([key, game]) => ({ key, game }))
);
const foundGames: FoundGame[] = [];
const gamesToScan = games.filter((g) => !g.game.executablePath);
for (const { key, game } of gamesToScan) {
const executableNames = GameExecutables.getExecutablesForGame(
game.objectId
);
if (!executableNames || executableNames.length === 0) continue;
const normalizedNames = new Set(
executableNames.map((name) => name.toLowerCase())
);
const foundPath = await searchInDirectories(normalizedNames);
if (foundPath) {
await gamesSublevel.put(key, { ...game, executablePath: foundPath });
logger.info(
`[ScanInstalledGames] Found executable for ${game.objectId}: ${foundPath}`
);
foundGames.push({ title: game.title, executablePath: foundPath });
}
}
WindowManager.mainWindow?.webContents.send("on-library-batch-complete");
await publishScanNotification(foundGames.length);
return { foundGames, total: gamesToScan.length };
};
async function findExecutableInFolder(
folderPath: string,
executableNames: Set<string>
): Promise<string | null> {
try {
const entries = await fs.promises.readdir(folderPath, {
withFileTypes: true,
recursive: true,
});
for (const entry of entries) {
if (!entry.isFile()) continue;
const fileName = entry.name.toLowerCase();
if (executableNames.has(fileName)) {
const parentPath =
"parentPath" in entry ? entry.parentPath : folderPath;
return path.join(parentPath, entry.name);
}
}
} catch (err) {
logger.error(
`[ScanInstalledGames] Error reading folder ${folderPath}:`,
err
);
}
return null;
}
registerEvent("scanInstalledGames", scanInstalledGames);

View File

@@ -1,12 +0,0 @@
import "./can-install-common-redist";
import "./check-homebrew-folder-exists";
import "./delete-temp-file";
import "./get-hydra-decky-plugin-info";
import "./hydra-api-call";
import "./install-common-redist";
import "./install-hydra-decky-plugin";
import "./open-checkout";
import "./open-external";
import "./save-temp-file";
import "./show-item-in-folder";
import "./show-open-dialog";

View File

@@ -1,8 +0,0 @@
import { registerEvent } from "../register-event";
import { LocalNotificationManager } from "@main/services";
const clearAllLocalNotifications = async () => {
await LocalNotificationManager.clearAll();
};
registerEvent("clearAllLocalNotifications", clearAllLocalNotifications);

View File

@@ -1,11 +0,0 @@
import { registerEvent } from "../register-event";
import { LocalNotificationManager } from "@main/services";
const deleteLocalNotification = async (
_event: Electron.IpcMainInvokeEvent,
id: string
) => {
await LocalNotificationManager.deleteNotification(id);
};
registerEvent("deleteLocalNotification", deleteLocalNotification);

View File

@@ -1,8 +0,0 @@
import { registerEvent } from "../register-event";
import { LocalNotificationManager } from "@main/services";
const getLocalNotificationsCount = async () => {
return LocalNotificationManager.getUnreadCount();
};
registerEvent("getLocalNotificationsCount", getLocalNotificationsCount);

View File

@@ -1,8 +0,0 @@
import { registerEvent } from "../register-event";
import { LocalNotificationManager } from "@main/services";
const getLocalNotifications = async () => {
return LocalNotificationManager.getNotifications();
};
registerEvent("getLocalNotifications", getLocalNotifications);

View File

@@ -1,9 +0,0 @@
import "./publish-new-repacks-notification";
import "./show-achievement-test-notification";
import "./update-achievement-notification-window";
import "./get-local-notifications";
import "./get-local-notifications-count";
import "./mark-local-notification-read";
import "./mark-all-local-notifications-read";
import "./delete-local-notification";
import "./clear-all-local-notifications";

View File

@@ -1,8 +0,0 @@
import { registerEvent } from "../register-event";
import { LocalNotificationManager } from "@main/services";
const markAllLocalNotificationsRead = async () => {
await LocalNotificationManager.markAllAsRead();
};
registerEvent("markAllLocalNotificationsRead", markAllLocalNotificationsRead);

View File

@@ -1,11 +0,0 @@
import { registerEvent } from "../register-event";
import { LocalNotificationManager } from "@main/services";
const markLocalNotificationRead = async (
_event: Electron.IpcMainInvokeEvent,
id: string
) => {
await LocalNotificationManager.markAsRead(id);
};
registerEvent("markLocalNotificationRead", markLocalNotificationRead);

View File

@@ -1,3 +0,0 @@
import "./get-me";
import "./process-profile-image";
import "./update-profile";

View File

@@ -1,20 +1,16 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { PythonRPC } from "@main/services/python-rpc"; import { PythonRPC } from "@main/services/python-rpc";
const processProfileImageEvent = async ( const processProfileImage = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
path: string path: string
) => { ) => {
return processProfileImage(path, "webp");
};
export const processProfileImage = async (path: string, extension?: string) => {
return PythonRPC.rpc return PythonRPC.rpc
.post<{ .post<{
imagePath: string; imagePath: string;
mimeType: string; mimeType: string;
}>("/profile-image", { image_path: path, target_extension: extension }) }>("/profile-image", { image_path: path })
.then((response) => response.data); .then((response) => response.data);
}; };
registerEvent("processProfileImage", processProfileImageEvent); registerEvent("processProfileImage", processProfileImage);

View File

@@ -0,0 +1,24 @@
import { registerEvent } from "../register-event";
import { HydraApi, WindowManager } from "@main/services";
import { UserNotLoggedInError } from "@shared";
import type { FriendRequestSync } from "@types";
export const syncFriendRequests = async () => {
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`)
.then((res) => {
WindowManager.mainWindow?.webContents.send(
"on-sync-friend-requests",
res
);
return res;
})
.catch((err) => {
if (err instanceof UserNotLoggedInError) {
return { friendRequestCount: 0 } as FriendRequestSync;
}
throw err;
});
};
registerEvent("syncFriendRequests", syncFriendRequests);

View File

@@ -51,30 +51,22 @@ const updateProfile = async (
"backgroundImageUrl", "backgroundImageUrl",
]); ]);
if (updateProfile.profileImageUrl !== undefined) { if (updateProfile.profileImageUrl) {
if (updateProfile.profileImageUrl === null) { const profileImageUrl = await uploadImage(
payload["profileImageUrl"] = null; "profile-image",
} else { updateProfile.profileImageUrl
const profileImageUrl = await uploadImage( ).catch(() => undefined);
"profile-image",
updateProfile.profileImageUrl
).catch(() => undefined);
payload["profileImageUrl"] = profileImageUrl; payload["profileImageUrl"] = profileImageUrl;
}
} }
if (updateProfile.backgroundImageUrl !== undefined) { if (updateProfile.backgroundImageUrl) {
if (updateProfile.backgroundImageUrl === null) { const backgroundImageUrl = await uploadImage(
payload["backgroundImageUrl"] = null; "background-image",
} else { updateProfile.backgroundImageUrl
const backgroundImageUrl = await uploadImage( ).catch(() => undefined);
"background-image",
updateProfile.backgroundImageUrl
).catch(() => undefined);
payload["backgroundImageUrl"] = backgroundImageUrl; payload["backgroundImageUrl"] = backgroundImageUrl;
}
} }
return patchUserProfile(payload); return patchUserProfile(payload);

View File

@@ -1,40 +0,0 @@
import { registerEvent } from "../register-event";
import fs from "node:fs";
import path from "node:path";
import { getThemePath } from "@main/helpers";
import { themesSublevel } from "@main/level";
const copyThemeAchievementSound = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string,
sourcePath: string
): Promise<void> => {
if (!sourcePath || !fs.existsSync(sourcePath)) {
throw new Error("Source file does not exist");
}
const theme = await themesSublevel.get(themeId);
if (!theme) {
throw new Error("Theme not found");
}
const themeDir = getThemePath(themeId, theme.name);
if (!fs.existsSync(themeDir)) {
fs.mkdirSync(themeDir, { recursive: true });
}
const fileExtension = path.extname(sourcePath);
const destinationPath = path.join(themeDir, `achievement${fileExtension}`);
await fs.promises.copyFile(sourcePath, destinationPath);
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: true,
originalSoundPath: sourcePath,
updatedAt: new Date(),
});
};
registerEvent("copyThemeAchievementSound", copyThemeAchievementSound);

View File

@@ -1,40 +0,0 @@
import { registerEvent } from "../register-event";
import { getThemeSoundPath } from "@main/helpers";
import { themesSublevel } from "@main/level";
import fs from "node:fs";
import path from "node:path";
import { logger } from "@main/services";
const getThemeSoundDataUrl = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
): Promise<string | null> => {
try {
const theme = await themesSublevel.get(themeId);
const soundPath = getThemeSoundPath(themeId, theme?.name);
if (!soundPath || !fs.existsSync(soundPath)) {
return null;
}
const buffer = await fs.promises.readFile(soundPath);
const ext = path.extname(soundPath).toLowerCase().slice(1);
const mimeTypes: Record<string, string> = {
mp3: "audio/mpeg",
wav: "audio/wav",
ogg: "audio/ogg",
m4a: "audio/mp4",
};
const mimeType = mimeTypes[ext] || "audio/mpeg";
const base64 = buffer.toString("base64");
return `data:${mimeType};base64,${base64}`;
} catch (error) {
logger.error("Failed to get theme sound data URL", error);
return null;
}
};
registerEvent("getThemeSoundDataUrl", getThemeSoundDataUrl);

View File

@@ -1,13 +0,0 @@
import { registerEvent } from "../register-event";
import { getThemeSoundPath } from "@main/helpers";
import { themesSublevel } from "@main/level";
const getThemeSoundPathEvent = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
): Promise<string | null> => {
const theme = await themesSublevel.get(themeId);
return getThemeSoundPath(themeId, theme?.name);
};
registerEvent("getThemeSoundPath", getThemeSoundPathEvent);

View File

@@ -1,60 +0,0 @@
import { registerEvent } from "../register-event";
import fs from "node:fs";
import path from "node:path";
import axios from "axios";
import { getThemePath } from "@main/helpers";
import { themesSublevel } from "@main/level";
import { logger } from "@main/services";
const importThemeSoundFromStore = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string,
themeName: string,
storeUrl: string
): Promise<void> => {
const theme = await themesSublevel.get(themeId);
if (!theme) {
throw new Error("Theme not found");
}
const formats = ["wav", "mp3", "ogg", "m4a"];
for (const format of formats) {
try {
const soundUrl = `${storeUrl}/themes/${themeName.toLowerCase()}/achievement.${format}`;
const response = await axios.get(soundUrl, {
responseType: "arraybuffer",
timeout: 10000,
});
const themeDir = getThemePath(themeId, theme.name);
if (!fs.existsSync(themeDir)) {
fs.mkdirSync(themeDir, { recursive: true });
}
const destinationPath = path.join(themeDir, `achievement.${format}`);
await fs.promises.writeFile(destinationPath, response.data);
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: true,
updatedAt: new Date(),
});
logger.log(`Successfully imported sound for theme ${themeName}`);
return;
} catch (error) {
logger.error(
`Failed to import ${format} sound for theme ${themeName}`,
error
);
continue;
}
}
logger.log(`No sound file found for theme ${themeName} in store`);
};
registerEvent("importThemeSoundFromStore", importThemeSoundFromStore);

View File

@@ -1,15 +0,0 @@
import "./add-custom-theme";
import "./close-editor-window";
import "./copy-theme-achievement-sound";
import "./delete-all-custom-themes";
import "./delete-custom-theme";
import "./get-active-custom-theme";
import "./get-all-custom-themes";
import "./get-custom-theme-by-id";
import "./get-theme-sound-data-url";
import "./get-theme-sound-path";
import "./import-theme-sound-from-store";
import "./open-editor-window";
import "./remove-theme-achievement-sound";
import "./toggle-custom-theme";
import "./update-custom-theme";

View File

@@ -1,48 +0,0 @@
import { registerEvent } from "../register-event";
import fs from "node:fs";
import { getThemePath } from "@main/helpers";
import { themesSublevel } from "@main/level";
import { THEMES_PATH } from "@main/constants";
import path from "node:path";
const removeThemeAchievementSound = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
): Promise<void> => {
const theme = await themesSublevel.get(themeId);
if (!theme) {
throw new Error("Theme not found");
}
const themeDir = getThemePath(themeId, theme.name);
const legacyThemeDir = path.join(THEMES_PATH, themeId);
const removeFromDir = async (dir: string) => {
if (!fs.existsSync(dir)) {
return;
}
const formats = ["wav", "mp3", "ogg", "m4a"];
for (const format of formats) {
const soundPath = path.join(dir, `achievement.${format}`);
if (fs.existsSync(soundPath)) {
await fs.promises.unlink(soundPath);
}
}
};
await removeFromDir(themeDir);
if (themeDir !== legacyThemeDir) {
await removeFromDir(legacyThemeDir);
}
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: false,
originalSoundPath: undefined,
updatedAt: new Date(),
});
};
registerEvent("removeThemeAchievementSound", removeThemeAchievementSound);

View File

@@ -1,78 +0,0 @@
import { registerEvent } from "../register-event";
import type { Download, StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services";
import { createGame } from "@main/services/library-sync";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { parseBytes } from "@shared";
import { handleDownloadError, prepareGameEntry } from "@main/helpers";
const addGameToQueue = async (
_event: Electron.IpcMainInvokeEvent,
payload: StartGameDownloadPayload
) => {
const {
objectId,
title,
shop,
downloadPath,
downloader,
uri,
automaticallyExtract,
fileSize,
} = payload;
const gameKey = levelKeys.game(shop, objectId);
const download: Download = {
shop,
objectId,
status: "paused",
progress: 0,
bytesDownloaded: 0,
downloadPath,
downloader,
uri,
folderName: null,
fileSize: parseBytes(fileSize ?? null),
shouldSeed: false,
timestamp: Date.now(),
queued: true,
extracting: false,
automaticallyExtract,
extractionProgress: 0,
};
try {
await DownloadManager.validateDownloadUrl(download);
} catch (err: unknown) {
logger.error("Failed to validate download URL for queue", err);
return handleDownloadError(err, downloader);
}
await prepareGameEntry({ gameKey, title, objectId, shop });
try {
await downloadsSublevel.put(gameKey, download);
const updatedGame = await gamesSublevel.get(gameKey);
await Promise.all([
createGame(updatedGame!).catch(() => {}),
HydraApi.post(`/games/${shop}/${objectId}/download`, null, {
needsAuth: false,
}).catch(() => {}),
]);
return { ok: true };
} catch (err: unknown) {
logger.error("Failed to add game to queue", err);
if (err instanceof Error) {
return { ok: false, error: err.message };
}
return { ok: false };
}
};
registerEvent("addGameToQueue", addGameToQueue);

View File

@@ -1,9 +0,0 @@
import "./add-game-to-queue";
import "./cancel-game-download";
import "./check-debrid-availability";
import "./pause-game-download";
import "./pause-game-seed";
import "./resume-game-download";
import "./resume-game-seed";
import "./start-game-download";
import "./update-download-queue-position";

View File

@@ -13,11 +13,7 @@ const resumeGameDownload = async (
const download = await downloadsSublevel.get(gameKey); const download = await downloadsSublevel.get(gameKey);
if ( if (download?.status === "paused") {
download &&
(download.status === "paused" || download.status === "active") &&
download.progress !== 1
) {
await DownloadManager.pauseDownload(); await DownloadManager.pauseDownload();
for await (const [key, value] of downloadsSublevel.iterator()) { for await (const [key, value] of downloadsSublevel.iterator()) {

View File

@@ -2,8 +2,14 @@ import { registerEvent } from "../register-event";
import type { Download, StartGameDownloadPayload } from "@types"; import type { Download, StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services"; import { DownloadManager, HydraApi, logger } from "@main/services";
import { createGame } from "@main/services/library-sync"; import { createGame } from "@main/services/library-sync";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { Downloader, DownloadError } from "@shared";
import { handleDownloadError, prepareGameEntry } from "@main/helpers"; import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
levelKeys,
} from "@main/level";
import { AxiosError } from "axios";
const startGameDownload = async ( const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@@ -32,7 +38,31 @@ const startGameDownload = async (
} }
} }
await prepareGameEntry({ gameKey, title, objectId, shop }); const game = await gamesSublevel.get(gameKey);
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
/* Delete any previous download */
await downloadsSublevel.del(gameKey);
if (game) {
await gamesSublevel.put(gameKey, {
...game,
isDeleted: false,
});
} else {
await gamesSublevel.put(gameKey, {
title,
iconUrl: gameAssets?.iconUrl ?? null,
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
logoImageUrl: gameAssets?.logoImageUrl ?? null,
objectId,
shop,
remoteId: null,
playTimeInMilliseconds: 0,
lastTimePlayed: null,
isDeleted: false,
});
}
await DownloadManager.cancelDownload(gameKey); await DownloadManager.cancelDownload(gameKey);
@@ -52,7 +82,6 @@ const startGameDownload = async (
queued: true, queued: true,
extracting: false, extracting: false,
automaticallyExtract, automaticallyExtract,
extractionProgress: 0,
}; };
try { try {
@@ -72,7 +101,32 @@ const startGameDownload = async (
return { ok: true }; return { ok: true };
} catch (err: unknown) { } catch (err: unknown) {
logger.error("Failed to start download", err); logger.error("Failed to start download", err);
return handleDownloadError(err, downloader);
if (err instanceof AxiosError) {
if (err.response?.status === 429 && downloader === Downloader.Gofile) {
return { ok: false, error: DownloadError.GofileQuotaExceeded };
}
if (
err.response?.status === 403 &&
downloader === Downloader.RealDebrid
) {
return {
ok: false,
error: DownloadError.RealDebridAccountNotAuthorized,
};
}
if (downloader === Downloader.TorBox) {
return { ok: false, error: err.response?.data?.detail };
}
}
if (err instanceof Error) {
return { ok: false, error: err.message };
}
return { ok: false };
} }
}; };

View File

@@ -1,67 +0,0 @@
import { registerEvent } from "../register-event";
import { downloadsSublevel, levelKeys } from "@main/level";
import { GameShop } from "@types";
import { orderBy } from "lodash-es";
const updateDownloadQueuePosition = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string,
direction: "up" | "down"
) => {
const gameKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(gameKey);
if (!download || !download.queued || download.status !== "paused") {
return false;
}
const allDownloads = await downloadsSublevel.values().all();
const queuedDownloads = orderBy(
allDownloads.filter((d) => d.status === "paused" && d.queued),
"timestamp",
"desc"
);
const currentIndex = queuedDownloads.findIndex(
(d) => d.shop === shop && d.objectId === objectId
);
if (currentIndex === -1) {
return false;
}
const targetIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
if (targetIndex < 0 || targetIndex >= queuedDownloads.length) {
return false;
}
const currentDownload = queuedDownloads[currentIndex];
const adjacentDownload = queuedDownloads[targetIndex];
const currentKey = levelKeys.game(
currentDownload.shop,
currentDownload.objectId
);
const adjacentKey = levelKeys.game(
adjacentDownload.shop,
adjacentDownload.objectId
);
const tempTimestamp = currentDownload.timestamp;
await downloadsSublevel.put(currentKey, {
...currentDownload,
timestamp: adjacentDownload.timestamp,
});
await downloadsSublevel.put(adjacentKey, {
...adjacentDownload,
timestamp: tempTimestamp,
});
return true;
};
registerEvent("updateDownloadQueuePosition", updateDownloadQueuePosition);

View File

@@ -1,5 +0,0 @@
import "./authenticate-real-debrid";
import "./authenticate-torbox";
import "./auto-launch";
import "./get-user-preferences";
import "./update-user-preferences";

View File

@@ -1,3 +0,0 @@
import "./get-auth";
import "./get-compared-unlocked-achievements";
import "./get-unlocked-achievements";

View File

@@ -1,4 +1,4 @@
// @generated by protobuf-ts 2.11.1 // @generated by protobuf-ts 2.10.0
// @generated from protobuf file "envelope.proto" (syntax proto3) // @generated from protobuf file "envelope.proto" (syntax proto3)
// tslint:disable // tslint:disable
import type { BinaryWriteOptions } from "@protobuf-ts/runtime"; import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
@@ -15,11 +15,11 @@ import { MessageType } from "@protobuf-ts/runtime";
*/ */
export interface FriendRequest { export interface FriendRequest {
/** /**
* @generated from protobuf field: int32 friend_request_count = 1 * @generated from protobuf field: int32 friend_request_count = 1;
*/ */
friendRequestCount: number; friendRequestCount: number;
/** /**
* @generated from protobuf field: optional string sender_id = 2 * @generated from protobuf field: optional string sender_id = 2;
*/ */
senderId?: string; senderId?: string;
} }
@@ -28,27 +28,18 @@ export interface FriendRequest {
*/ */
export interface FriendGameSession { export interface FriendGameSession {
/** /**
* @generated from protobuf field: string object_id = 1 * @generated from protobuf field: string object_id = 1;
*/ */
objectId: string; objectId: string;
/** /**
* @generated from protobuf field: string shop = 2 * @generated from protobuf field: string shop = 2;
*/ */
shop: string; shop: string;
/** /**
* @generated from protobuf field: string friend_id = 3 * @generated from protobuf field: string friend_id = 3;
*/ */
friendId: string; friendId: string;
} }
/**
* @generated from protobuf message Notification
*/
export interface Notification {
/**
* @generated from protobuf field: int32 notification_count = 1
*/
notificationCount: number;
}
/** /**
* @generated from protobuf message Envelope * @generated from protobuf message Envelope
*/ */
@@ -60,24 +51,17 @@ export interface Envelope {
| { | {
oneofKind: "friendRequest"; oneofKind: "friendRequest";
/** /**
* @generated from protobuf field: FriendRequest friend_request = 1 * @generated from protobuf field: FriendRequest friend_request = 1;
*/ */
friendRequest: FriendRequest; friendRequest: FriendRequest;
} }
| { | {
oneofKind: "friendGameSession"; oneofKind: "friendGameSession";
/** /**
* @generated from protobuf field: FriendGameSession friend_game_session = 2 * @generated from protobuf field: FriendGameSession friend_game_session = 2;
*/ */
friendGameSession: FriendGameSession; friendGameSession: FriendGameSession;
} }
| {
oneofKind: "notification";
/**
* @generated from protobuf field: Notification notification = 3
*/
notification: Notification;
}
| { | {
oneofKind: undefined; oneofKind: undefined;
}; };
@@ -255,80 +239,6 @@ class FriendGameSession$Type extends MessageType<FriendGameSession> {
*/ */
export const FriendGameSession = new FriendGameSession$Type(); export const FriendGameSession = new FriendGameSession$Type();
// @generated message type with reflection information, may provide speed optimized methods // @generated message type with reflection information, may provide speed optimized methods
class Notification$Type extends MessageType<Notification> {
constructor() {
super("Notification", [
{
no: 1,
name: "notification_count",
kind: "scalar",
T: 5 /*ScalarType.INT32*/,
},
]);
}
create(value?: PartialMessage<Notification>): Notification {
const message = globalThis.Object.create(this.messagePrototype!);
message.notificationCount = 0;
if (value !== undefined)
reflectionMergePartial<Notification>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: Notification
): Notification {
let message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 notification_count */ 1:
message.notificationCount = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
}
return message;
}
internalBinaryWrite(
message: Notification,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* int32 notification_count = 1; */
if (message.notificationCount !== 0)
writer.tag(1, WireType.Varint).int32(message.notificationCount);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message Notification
*/
export const Notification = new Notification$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Envelope$Type extends MessageType<Envelope> { class Envelope$Type extends MessageType<Envelope> {
constructor() { constructor() {
super("Envelope", [ super("Envelope", [
@@ -346,13 +256,6 @@ class Envelope$Type extends MessageType<Envelope> {
oneof: "payload", oneof: "payload",
T: () => FriendGameSession, T: () => FriendGameSession,
}, },
{
no: 3,
name: "notification",
kind: "message",
oneof: "payload",
T: () => Notification,
},
]); ]);
} }
create(value?: PartialMessage<Envelope>): Envelope { create(value?: PartialMessage<Envelope>): Envelope {
@@ -395,17 +298,6 @@ class Envelope$Type extends MessageType<Envelope> {
), ),
}; };
break; break;
case /* Notification notification */ 3:
message.payload = {
oneofKind: "notification",
notification: Notification.internalBinaryRead(
reader,
reader.uint32(),
options,
(message.payload as any).notification
),
};
break;
default: default:
let u = options.readUnknownField; let u = options.readUnknownField;
if (u === "throw") if (u === "throw")
@@ -444,13 +336,6 @@ class Envelope$Type extends MessageType<Envelope> {
writer.tag(2, WireType.LengthDelimited).fork(), writer.tag(2, WireType.LengthDelimited).fork(),
options options
).join(); ).join();
/* Notification notification = 3; */
if (message.payload.oneofKind === "notification")
Notification.internalBinaryWrite(
message.payload.notification,
writer.tag(3, WireType.LengthDelimited).fork(),
options
).join();
let u = options.writeUnknownFields; let u = options.writeUnknownFields;
if (u !== false) if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)( (u == true ? UnknownFieldHandler.onWrite : u)(

View File

@@ -1,51 +0,0 @@
import { AxiosError } from "axios";
import { Downloader, DownloadError } from "@shared";
export const handleDownloadError = (
err: unknown,
downloader: Downloader
): { ok: false; error?: string } => {
if (err instanceof AxiosError) {
if (err.response?.status === 429 && downloader === Downloader.Gofile) {
return { ok: false, error: DownloadError.GofileQuotaExceeded };
}
if (err.response?.status === 403 && downloader === Downloader.RealDebrid) {
return { ok: false, error: DownloadError.RealDebridAccountNotAuthorized };
}
if (downloader === Downloader.TorBox) {
return { ok: false, error: err.response?.data?.detail };
}
}
if (err instanceof Error) {
if (downloader === Downloader.Buzzheavier) {
if (err.message.includes("Rate limit")) {
return { ok: false, error: "Buzzheavier: Rate limit exceeded" };
}
if (
err.message.includes("not found") ||
err.message.includes("deleted")
) {
return { ok: false, error: "Buzzheavier: File not found" };
}
}
if (downloader === Downloader.FuckingFast) {
if (err.message.includes("Rate limit")) {
return { ok: false, error: "FuckingFast: Rate limit exceeded" };
}
if (
err.message.includes("not found") ||
err.message.includes("deleted")
) {
return { ok: false, error: "FuckingFast: File not found" };
}
}
return { ok: false, error: err.message };
}
return { ok: false };
};

View File

@@ -1,45 +0,0 @@
import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
} from "@main/level";
import type { GameShop } from "@types";
interface PrepareGameEntryParams {
gameKey: string;
title: string;
objectId: string;
shop: GameShop;
}
export const prepareGameEntry = async ({
gameKey,
title,
objectId,
shop,
}: PrepareGameEntryParams): Promise<void> => {
const game = await gamesSublevel.get(gameKey);
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
await downloadsSublevel.del(gameKey);
if (game) {
await gamesSublevel.put(gameKey, {
...game,
isDeleted: false,
});
} else {
await gamesSublevel.put(gameKey, {
title,
iconUrl: gameAssets?.iconUrl ?? null,
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
logoImageUrl: gameAssets?.logoImageUrl ?? null,
objectId,
shop,
remoteId: null,
playTimeInMilliseconds: 0,
lastTimePlayed: null,
isDeleted: false,
});
}
};

View File

@@ -2,8 +2,6 @@ import axios from "axios";
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import UserAgent from "user-agents"; import UserAgent from "user-agents";
import path from "node:path"; import path from "node:path";
import fs from "node:fs";
import { THEMES_PATH } from "@main/constants";
export const getFileBuffer = async (url: string) => export const getFileBuffer = async (url: string) =>
fetch(url, { method: "GET" }).then((response) => fetch(url, { method: "GET" }).then((response) =>
@@ -33,66 +31,9 @@ export const isPortableVersion = () => {
}; };
export const normalizePath = (str: string) => export const normalizePath = (str: string) =>
path.posix.normalize(str).replaceAll("\\", "/"); path.posix.normalize(str).replace(/\\/g, "/");
export const addTrailingSlash = (str: string) => export const addTrailingSlash = (str: string) =>
str.endsWith("/") ? str : `${str}/`; str.endsWith("/") ? str : `${str}/`;
const sanitizeFolderName = (name: string): string => {
return name
.toLowerCase()
.replaceAll(/[^a-z0-9-_\s]/g, "")
.replaceAll(/\s+/g, "-")
.replaceAll(/-+/g, "-")
.replaceAll(/(^-|-$)/g, "");
};
export const getThemePath = (themeId: string, themeName?: string): string => {
if (themeName) {
const sanitizedName = sanitizeFolderName(themeName);
if (sanitizedName) {
return path.join(THEMES_PATH, sanitizedName);
}
}
return path.join(THEMES_PATH, themeId);
};
export const getThemeSoundPath = (
themeId: string,
themeName?: string
): string | null => {
const themeDir = getThemePath(themeId, themeName);
const legacyThemeDir = themeName ? path.join(THEMES_PATH, themeId) : null;
const checkDir = (dir: string): string | null => {
if (!fs.existsSync(dir)) {
return null;
}
const formats = ["wav", "mp3", "ogg", "m4a"];
for (const format of formats) {
const soundPath = path.join(dir, `achievement.${format}`);
if (fs.existsSync(soundPath)) {
return soundPath;
}
}
return null;
};
const soundPath = checkDir(themeDir);
if (soundPath) {
return soundPath;
}
if (legacyThemeDir) {
return checkDir(legacyThemeDir);
}
return null;
};
export * from "./reg-parser"; export * from "./reg-parser";
export * from "./download-error-handler";
export * from "./download-game-helper";

View File

@@ -1,67 +0,0 @@
import { levelKeys } from "./keys";
import { db } from "../level";
import { logger } from "@main/services";
// Gets when we last started the app (for next API call's 'since')
export const getDownloadSourcesCheckBaseline = async (): Promise<
string | null
> => {
try {
const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline, {
valueEncoding: "utf8",
});
return timestamp;
} catch (error) {
if (error instanceof Error && error.name === "NotFoundError") {
logger.debug("Download sources check baseline not found, returning null");
} else {
logger.error(
"Unexpected error while getting download sources check baseline",
error
);
}
return null;
}
};
// Updates to current time (when app starts)
export const updateDownloadSourcesCheckBaseline = async (
timestamp: string
): Promise<void> => {
const utcTimestamp = new Date(timestamp).toISOString();
await db.put(levelKeys.downloadSourcesCheckBaseline, utcTimestamp, {
valueEncoding: "utf8",
});
};
// Gets the 'since' value the API used in the last check (for modal comparison)
export const getDownloadSourcesSinceValue = async (): Promise<
string | null
> => {
try {
const timestamp = await db.get(levelKeys.downloadSourcesSinceValue, {
valueEncoding: "utf8",
});
return timestamp;
} catch (error) {
if (error instanceof Error && error.name === "NotFoundError") {
logger.debug("Download sources since value not found, returning null");
} else {
logger.error(
"Unexpected error while getting download sources since value",
error
);
}
return null;
}
};
// Saves the 'since' value we used in the API call (for modal to compare against)
export const updateDownloadSourcesSinceValue = async (
timestamp: string
): Promise<void> => {
const utcTimestamp = new Date(timestamp).toISOString();
await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp, {
valueEncoding: "utf8",
});
};

View File

@@ -7,5 +7,3 @@ export * from "./game-achievements";
export * from "./keys"; export * from "./keys";
export * from "./themes"; export * from "./themes";
export * from "./download-sources"; export * from "./download-sources";
export * from "./downloadSourcesCheckTimestamp";
export * from "./local-notifications";

View File

@@ -18,7 +18,4 @@ export const levelKeys = {
screenState: "screenState", screenState: "screenState",
rpcPassword: "rpcPassword", rpcPassword: "rpcPassword",
downloadSources: "downloadSources", downloadSources: "downloadSources",
downloadSourcesCheckBaseline: "downloadSourcesCheckBaseline", // When we last started the app
downloadSourcesSinceValue: "downloadSourcesSinceValue", // The 'since' value API used (for modal comparison)
localNotifications: "localNotifications",
}; };

View File

@@ -1,11 +0,0 @@
import type { LocalNotification } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
export const localNotificationsSublevel = db.sublevel<
string,
LocalNotification
>(levelKeys.localNotifications, {
valueEncoding: "json",
});

View File

@@ -1,8 +1,8 @@
import { downloadsSublevel } from "./level/sublevels/downloads"; import { downloadsSublevel } from "./level/sublevels/downloads";
import { orderBy } from "lodash-es"; import { sortBy } from "lodash-es";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import { levelKeys, db } from "./level"; import { levelKeys, db } from "./level";
import type { Download, UserPreferences } from "@types"; import type { UserPreferences } from "@types";
import { import {
SystemPath, SystemPath,
CommonRedistManager, CommonRedistManager,
@@ -16,9 +16,7 @@ import {
Ludusavi, Ludusavi,
Lock, Lock,
DeckyPlugin, DeckyPlugin,
DownloadSourcesChecker,
WSClient, WSClient,
logger,
} from "@main/services"; } from "@main/services";
import { migrateDownloadSources } from "./helpers/migrate-download-sources"; import { migrateDownloadSources } from "./helpers/migrate-download-sources";
@@ -34,7 +32,9 @@ export const loadState = async () => {
await import("./events"); await import("./events");
Aria2.spawn(); if (process.platform !== "darwin") {
Aria2.spawn();
}
if (userPreferences?.realDebridApiToken) { if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences.realDebridApiToken); RealDebridClient.authorize(userPreferences.realDebridApiToken);
@@ -57,11 +57,6 @@ export const loadState = async () => {
const { syncDownloadSourcesFromApi } = await import("./services/user"); const { syncDownloadSourcesFromApi } = await import("./services/user");
void syncDownloadSourcesFromApi(); void syncDownloadSourcesFromApi();
// Check for new download options on startup (if enabled)
(async () => {
await DownloadSourcesChecker.checkForChanges();
})();
WSClient.connect(); WSClient.connect();
}); });
@@ -69,50 +64,21 @@ export const loadState = async () => {
.values() .values()
.all() .all()
.then((games) => { .then((games) => {
return orderBy(games, "timestamp", "desc"); return sortBy(games, "timestamp", "DESC");
}); });
let interruptedDownload: Download | null = null; downloads.forEach((download) => {
for (const download of downloads) {
const downloadKey = levelKeys.game(download.shop, download.objectId);
// Reset extracting state
if (download.extracting) { if (download.extracting) {
await downloadsSublevel.put(downloadKey, { downloadsSublevel.put(levelKeys.game(download.shop, download.objectId), {
...download, ...download,
extracting: false, extracting: false,
}); });
} }
});
// Find interrupted active download (download that was running when app closed) const [nextItemOnQueue] = downloads.filter((game) => game.queued);
// Mark it as paused but remember it for auto-resume
if (download.status === "active" && !interruptedDownload) {
interruptedDownload = download;
await downloadsSublevel.put(downloadKey, {
...download,
status: "paused",
});
} else if (download.status === "active") {
// Mark other active downloads as paused
await downloadsSublevel.put(downloadKey, {
...download,
status: "paused",
});
}
}
// Re-fetch downloads after status updates const downloadsToSeed = downloads.filter(
const updatedDownloads = await downloadsSublevel
.values()
.all()
.then((games) => orderBy(games, "timestamp", "desc"));
// Prioritize interrupted download, then queued downloads
const downloadToResume =
interruptedDownload ?? updatedDownloads.find((game) => game.queued);
const downloadsToSeed = updatedDownloads.filter(
(game) => (game) =>
game.shouldSeed && game.shouldSeed &&
game.downloader === Downloader.Torrent && game.downloader === Downloader.Torrent &&
@@ -120,23 +86,7 @@ export const loadState = async () => {
game.uri !== null game.uri !== null
); );
// For torrents or if JS downloader is disabled, use Python RPC await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
const isTorrent = downloadToResume?.downloader === Downloader.Torrent;
// Default to true - native HTTP downloader is enabled by default
const useJsDownloader =
(userPreferences?.useNativeHttpDownloader ?? true) && !isTorrent;
if (useJsDownloader && downloadToResume) {
// Start Python RPC for seeding only, then resume HTTP download with JS
await DownloadManager.startRPC(undefined, downloadsToSeed);
await DownloadManager.startDownload(downloadToResume).catch((err) => {
// If resume fails, just log it - user can manually retry
logger.error("Failed to auto-resume download:", err);
});
} else {
// Use Python RPC for everything (torrent or fallback)
await DownloadManager.startRPC(downloadToResume, downloadsToSeed);
}
startMainLoop(); startMainLoop();

View File

@@ -1,5 +1,5 @@
import { app } from "electron"; import { app } from "electron";
import Seven, { CommandLineSwitches } from "node-7z"; import cp from "node:child_process";
import path from "node:path"; import path from "node:path";
import { logger } from "./logger"; import { logger } from "./logger";
@@ -9,17 +9,6 @@ export const binaryName = {
win32: "7z.exe", win32: "7z.exe",
}; };
export interface ExtractionProgress {
percent: number;
fileCount: number;
file: string;
}
export interface ExtractionResult {
success: boolean;
extractedFiles: string[];
}
export class SevenZip { export class SevenZip {
private static readonly binaryPath = app.isPackaged private static readonly binaryPath = app.isPackaged
? path.join(process.resourcesPath, binaryName[process.platform]) ? path.join(process.resourcesPath, binaryName[process.platform])
@@ -43,109 +32,43 @@ export class SevenZip {
cwd?: string; cwd?: string;
passwords?: string[]; passwords?: string[];
}, },
onProgress?: (progress: ExtractionProgress) => void successCb: () => void,
): Promise<ExtractionResult> { errorCb: () => void
return new Promise((resolve, reject) => { ) {
const tryPassword = (index = 0) => { const tryPassword = (index = -1) => {
const password = passwords[index] ?? ""; const password = passwords[index] ?? "";
logger.info( logger.info(`Trying password ${password} on ${filePath}`);
`Trying password "${password || "(empty)"}" on ${filePath}`
);
const extractedFiles: string[] = []; const args = ["x", filePath, "-y", "-p" + password];
let fileCount = 0;
const options: CommandLineSwitches = { if (outputPath) {
$bin: this.binaryPath, args.push("-o" + outputPath);
$progress: true, }
yes: true,
password: password || undefined,
};
if (outputPath) { const child = cp.execFile(this.binaryPath, args, {
options.outputDir = outputPath; cwd,
});
child.once("exit", (code) => {
if (code === 0) {
successCb();
return;
} }
const stream = Seven.extractFull(filePath, outputPath || cwd || ".", { if (index < passwords.length - 1) {
...options,
$spawnOptions: cwd ? { cwd } : undefined,
});
stream.on("progress", (progress) => {
if (onProgress) {
onProgress({
percent: progress.percent,
fileCount: fileCount,
file: progress.fileCount?.toString() || "",
});
}
});
stream.on("data", (data) => {
if (data.file) {
extractedFiles.push(data.file);
fileCount++;
}
});
stream.on("end", () => {
logger.info( logger.info(
`Successfully extracted ${filePath} (${extractedFiles.length} files)` `Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
); );
resolve({
success: true,
extractedFiles,
});
});
stream.on("error", (err) => { tryPassword(index + 1);
logger.error(`Extraction error for ${filePath}:`, err); } else {
logger.info(`Failed to extract file: ${filePath}`);
if (index < passwords.length - 1) { errorCb();
logger.info(
`Failed to extract file: ${filePath} with password: "${password}". Trying next password...`
);
tryPassword(index + 1);
} else {
logger.error(
`Failed to extract file: ${filePath} after trying all passwords`
);
reject(new Error(`Failed to extract file: ${filePath}`));
}
});
};
tryPassword(0);
});
}
public static listFiles(
filePath: string,
password?: string
): Promise<string[]> {
return new Promise((resolve, reject) => {
const files: string[] = [];
const options: CommandLineSwitches = {
$bin: this.binaryPath,
password: password || undefined,
};
const stream = Seven.list(filePath, options);
stream.on("data", (data) => {
if (data.file) {
files.push(data.file);
} }
}); });
};
stream.on("end", () => { tryPassword();
resolve(files);
});
stream.on("error", (err) => {
reject(err);
});
});
} }
} }

View File

@@ -7,12 +7,9 @@ export class Aria2 {
private static process: cp.ChildProcess | null = null; private static process: cp.ChildProcess | null = null;
public static spawn() { public static spawn() {
const binaryPath = const binaryPath = app.isPackaged
process.platform === "darwin" ? path.join(process.resourcesPath, "aria2c")
? "aria2c" : path.join(__dirname, "..", "..", "binaries", "aria2c");
: app.isPackaged
? path.join(process.resourcesPath, "aria2c")
: path.join(__dirname, "..", "..", "binaries", "aria2c");
this.process = cp.spawn( this.process = cp.spawn(
binaryPath, binaryPath,

View File

@@ -74,16 +74,21 @@ export class DeckyPlugin {
await fs.promises.mkdir(extractPath, { recursive: true }); await fs.promises.mkdir(extractPath, { recursive: true });
try { return new Promise((resolve, reject) => {
await SevenZip.extractFile({ SevenZip.extractFile(
filePath: zipPath, {
outputPath: extractPath, filePath: zipPath,
}); outputPath: extractPath,
logger.log(`Plugin extracted to: ${extractPath}`); },
return extractPath; () => {
} catch { logger.log(`Plugin extracted to: ${extractPath}`);
throw new Error("Failed to extract plugin"); resolve(extractPath);
} },
() => {
reject(new Error("Failed to extract plugin"));
}
);
});
} }
private static needsSudo(): boolean { private static needsSudo(): boolean {

View File

@@ -1,204 +0,0 @@
import { HydraApi } from "./hydra-api";
import {
gamesSublevel,
getDownloadSourcesCheckBaseline,
updateDownloadSourcesCheckBaseline,
updateDownloadSourcesSinceValue,
downloadSourcesSublevel,
db,
levelKeys,
} from "@main/level";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
import type { Game, UserPreferences } from "@types";
interface DownloadSourcesChangeResponse {
shop: string;
objectId: string;
newDownloadOptionsCount: number;
downloadSourceIds: string[];
}
export class DownloadSourcesChecker {
private static async clearStaleBadges(
nonCustomGames: Game[]
): Promise<{ gameId: string; count: number }[]> {
const previouslyFlaggedGames = nonCustomGames.filter(
(game: Game) =>
game.newDownloadOptionsCount && game.newDownloadOptionsCount > 0
);
const clearedPayload: { gameId: string; count: number }[] = [];
if (previouslyFlaggedGames.length > 0) {
logger.info(
`Clearing stale newDownloadOptionsCount for ${previouslyFlaggedGames.length} games`
);
for (const game of previouslyFlaggedGames) {
await gamesSublevel.put(`${game.shop}:${game.objectId}`, {
...game,
newDownloadOptionsCount: undefined,
});
clearedPayload.push({
gameId: `${game.shop}:${game.objectId}`,
count: 0,
});
}
}
return clearedPayload;
}
private static async processApiResponse(
response: unknown,
nonCustomGames: Game[]
): Promise<{ gameId: string; count: number }[]> {
if (!response || !Array.isArray(response)) {
return [];
}
const gamesWithNewOptions: { gameId: string; count: number }[] = [];
for (const gameUpdate of response as DownloadSourcesChangeResponse[]) {
if (gameUpdate.newDownloadOptionsCount > 0) {
const game = nonCustomGames.find(
(g) =>
g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId
);
if (game) {
await gamesSublevel.put(`${game.shop}:${game.objectId}`, {
...game,
newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount,
});
gamesWithNewOptions.push({
gameId: `${game.shop}:${game.objectId}`,
count: gameUpdate.newDownloadOptionsCount,
});
}
}
}
return gamesWithNewOptions;
}
private static sendNewDownloadOptionsEvent(
clearedPayload: { gameId: string; count: number }[],
gamesWithNewOptions: { gameId: string; count: number }[]
): void {
const eventPayload = [...clearedPayload, ...gamesWithNewOptions];
if (eventPayload.length > 0 && WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send(
"on-new-download-options",
eventPayload
);
}
logger.info(
`Found new download options for ${gamesWithNewOptions.length} games`
);
}
static async checkForChanges(): Promise<void> {
logger.info("DownloadSourcesChecker.checkForChanges() called");
try {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (userPreferences?.enableNewDownloadOptionsBadges === false) {
logger.info(
"New download options badges are disabled, skipping download sources check"
);
return;
}
// Get all installed games (excluding custom games)
const installedGames = await gamesSublevel.values().all();
const nonCustomGames = installedGames.filter(
(game: Game) => game.shop !== "custom"
);
logger.info(
`Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games`
);
if (nonCustomGames.length === 0) {
logger.info(
"No non-custom games found, skipping download sources check"
);
return;
}
const downloadSources = await downloadSourcesSublevel.values().all();
const downloadSourceIds = downloadSources.map((source) => source.id);
logger.info(
`Found ${downloadSourceIds.length} download sources: ${downloadSourceIds.join(", ")}`
);
if (downloadSourceIds.length === 0) {
logger.info(
"No download sources found, skipping download sources check"
);
return;
}
const previousBaseline = await getDownloadSourcesCheckBaseline();
const since =
previousBaseline ||
new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
logger.info(`Using since: ${since} (from last app start)`);
const clearedPayload = await this.clearStaleBadges(nonCustomGames);
const games = nonCustomGames.map((game: Game) => ({
shop: game.shop,
objectId: game.objectId,
}));
logger.info(
`Checking download sources changes for ${games.length} non-custom games since ${since}`
);
logger.info(
`Making API call to HydraApi.checkDownloadSourcesChanges with:`,
{
downloadSourceIds,
gamesCount: games.length,
since,
}
);
const response = await HydraApi.checkDownloadSourcesChanges(
downloadSourceIds,
games,
since
);
logger.info("API call completed, response:", response);
await updateDownloadSourcesSinceValue(since);
logger.info(`Saved 'since' value: ${since} (for modal comparison)`);
const now = new Date().toISOString();
await updateDownloadSourcesCheckBaseline(now);
logger.info(
`Updated baseline to: ${now} (will be 'since' on next app start)`
);
const gamesWithNewOptions = await this.processApiResponse(
response,
nonCustomGames
);
this.sendNewDownloadOptionsEvent(clearedPayload, gamesWithNewOptions);
logger.info("Download sources check completed successfully");
} catch (error) {
logger.error("Failed to check download sources changes:", error);
}
}
}

View File

@@ -4,11 +4,10 @@ import { publishDownloadCompleteNotification } from "../notifications";
import type { Download, DownloadProgress, UserPreferences } from "@types"; import type { Download, DownloadProgress, UserPreferences } from "@types";
import { import {
GofileApi, GofileApi,
QiwiApi,
DatanodesApi, DatanodesApi,
MediafireApi, MediafireApi,
PixelDrainApi, PixelDrainApi,
VikingFileApi,
RootzApi,
} from "../hosters"; } from "../hosters";
import { PythonRPC } from "../python-rpc"; import { PythonRPC } from "../python-rpc";
import { import {
@@ -18,131 +17,16 @@ import {
} from "./types"; } from "./types";
import { calculateETA, getDirSize } from "./helpers"; import { calculateETA, getDirSize } from "./helpers";
import { RealDebridClient } from "./real-debrid"; import { RealDebridClient } from "./real-debrid";
import path from "node:path"; import path from "path";
import { logger } from "../logger"; import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { orderBy } from "lodash-es"; import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox"; import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager"; import { GameFilesManager } from "../game-files-manager";
import { HydraDebridClient } from "./hydra-debrid"; import { HydraDebridClient } from "./hydra-debrid";
import { BuzzheavierApi, FuckingFastApi } from "@main/services/hosters";
import { JsHttpDownloader } from "./js-http-downloader";
export class DownloadManager { export class DownloadManager {
private static downloadingGameId: string | null = null; private static downloadingGameId: string | null = null;
private static jsDownloader: JsHttpDownloader | null = null;
private static usingJsDownloader = false;
private static isPreparingDownload = false;
private static extractFilename(
url: string,
originalUrl?: string
): string | undefined {
if (originalUrl?.includes("#")) {
const hashPart = originalUrl.split("#")[1];
if (hashPart && !hashPart.startsWith("http") && hashPart.includes(".")) {
return hashPart;
}
}
if (url.includes("#")) {
const hashPart = url.split("#")[1];
if (hashPart && !hashPart.startsWith("http") && hashPart.includes(".")) {
return hashPart;
}
}
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const pathParts = pathname.split("/");
const filename = pathParts.at(-1);
if (filename?.includes(".") && filename.length > 0) {
return decodeURIComponent(filename);
}
} catch {
// Invalid URL
}
return undefined;
}
private static sanitizeFilename(filename: string): string {
return filename.replaceAll(/[<>:"/\\|?*]/g, "_");
}
private static resolveFilename(
resumingFilename: string | undefined,
originalUrl: string,
downloadUrl: string
): string | undefined {
if (resumingFilename) return resumingFilename;
const extracted =
this.extractFilename(originalUrl, downloadUrl) ||
this.extractFilename(downloadUrl);
return extracted ? this.sanitizeFilename(extracted) : undefined;
}
private static buildDownloadOptions(
url: string,
savePath: string,
filename: string | undefined,
headers?: Record<string, string>
) {
return {
url,
savePath,
filename,
headers,
};
}
private static createDownloadPayload(
directUrl: string,
originalUrl: string,
downloadId: string,
savePath: string
) {
const filename =
this.extractFilename(originalUrl, directUrl) ||
this.extractFilename(directUrl);
const sanitizedFilename = filename
? this.sanitizeFilename(filename)
: undefined;
if (sanitizedFilename) {
logger.log(`[DownloadManager] Using filename: ${sanitizedFilename}`);
} else {
logger.log(
`[DownloadManager] No filename extracted, aria2 will use default`
);
}
return {
action: "start" as const,
game_id: downloadId,
url: directUrl,
save_path: savePath,
out: sanitizedFilename,
allow_multiple_connections: true,
};
}
private static async shouldUseJsDownloader(): Promise<boolean> {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{ valueEncoding: "json" }
);
// Default to true - native HTTP downloader is enabled by default (opt-out)
return userPreferences?.useNativeHttpDownloader ?? true;
}
private static isHttpDownloader(downloader: Downloader): boolean {
return downloader !== Downloader.Torrent;
}
public static async startRPC( public static async startRPC(
download?: Download, download?: Download,
@@ -168,87 +52,7 @@ export class DownloadManager {
} }
} }
private static async getDownloadStatusFromJs(): Promise<DownloadProgress | null> { private static async getDownloadStatus() {
if (!this.downloadingGameId) return null;
const downloadId = this.downloadingGameId;
// Return a "preparing" status while fetching download options
if (this.isPreparingDownload) {
try {
const download = await downloadsSublevel.get(downloadId);
if (!download) return null;
return {
numPeers: 0,
numSeeds: 0,
downloadSpeed: 0,
timeRemaining: -1,
isDownloadingMetadata: true, // Use this to indicate "preparing"
isCheckingFiles: false,
progress: 0,
gameId: downloadId,
download,
};
} catch {
return null;
}
}
if (!this.jsDownloader) return null;
const status = this.jsDownloader.getDownloadStatus();
if (!status) return null;
try {
const download = await downloadsSublevel.get(downloadId);
if (!download) return null;
const { progress, downloadSpeed, bytesDownloaded, fileSize, folderName } =
status;
// Only update fileSize in database if we actually know it (> 0)
// Otherwise keep the existing value to avoid showing "0 B"
const effectiveFileSize = fileSize > 0 ? fileSize : download.fileSize;
const updatedDownload = {
...download,
bytesDownloaded,
fileSize: effectiveFileSize,
progress,
folderName,
status:
status.status === "complete"
? ("complete" as const)
: ("active" as const),
};
if (status.status === "active" || status.status === "complete") {
await downloadsSublevel.put(downloadId, updatedDownload);
}
return {
numPeers: 0,
numSeeds: 0,
downloadSpeed,
timeRemaining: calculateETA(
effectiveFileSize ?? 0,
bytesDownloaded,
downloadSpeed
),
isDownloadingMetadata: false,
isCheckingFiles: false,
progress,
gameId: downloadId,
download: updatedDownload,
};
} catch (err) {
logger.error("[DownloadManager] Error getting JS download status:", err);
return null;
}
}
private static async getDownloadStatusFromRpc(): Promise<DownloadProgress | null> {
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>( const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
"/status" "/status"
); );
@@ -297,147 +101,114 @@ export class DownloadManager {
gameId: downloadId, gameId: downloadId,
download, download,
} as DownloadProgress; } as DownloadProgress;
} catch { } catch (err) {
return null; return null;
} }
} }
private static async getDownloadStatus(): Promise<DownloadProgress | null> {
if (this.usingJsDownloader) {
return this.getDownloadStatusFromJs();
}
return this.getDownloadStatusFromRpc();
}
public static async watchDownloads() { public static async watchDownloads() {
const status = await this.getDownloadStatus(); const status = await this.getDownloadStatus();
if (!status) return;
const { gameId, progress } = status; if (status) {
const [download, game] = await Promise.all([ const { gameId, progress } = status;
downloadsSublevel.get(gameId),
gamesSublevel.get(gameId),
]);
if (!download || !game) return; const [download, game] = await Promise.all([
downloadsSublevel.get(gameId),
gamesSublevel.get(gameId),
]);
this.sendProgressUpdate(progress, status, game); if (!download || !game) return;
const isComplete = progress === 1 || download.status === "complete"; const userPreferences = await db.get<string, UserPreferences | null>(
if (isComplete) { levelKeys.userPreferences,
await this.handleDownloadCompletion(download, game, gameId); {
} valueEncoding: "json",
} }
private static sendProgressUpdate(
progress: number,
status: DownloadProgress,
game: any
) {
if (WindowManager.mainWindow) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
structuredClone({ ...status, game })
);
}
}
private static async handleDownloadCompletion(
download: Download,
game: any,
gameId: string
) {
publishDownloadCompleteNotification(game);
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{ valueEncoding: "json" }
);
await this.updateDownloadStatus(
download,
gameId,
userPreferences?.seedAfterDownloadComplete
);
if (download.automaticallyExtract) {
this.handleExtraction(download, game);
} else {
// For downloads without extraction (e.g., torrents with ready-to-play files),
// search for executable in the download folder
const gameFilesManager = new GameFilesManager(game.shop, game.objectId);
gameFilesManager.searchAndBindExecutable();
}
await this.processNextQueuedDownload();
}
private static async updateDownloadStatus(
download: Download,
gameId: string,
shouldSeed?: boolean
) {
const shouldExtract = download.automaticallyExtract;
if (shouldSeed && download.downloader === Downloader.Torrent) {
await downloadsSublevel.put(gameId, {
...download,
status: "seeding",
shouldSeed: true,
queued: false,
extracting: shouldExtract,
});
} else {
await downloadsSublevel.put(gameId, {
...download,
status: "complete",
shouldSeed: false,
queued: false,
extracting: shouldExtract,
});
this.cancelDownload(gameId);
}
}
private static handleExtraction(download: Download, game: any) {
const gameFilesManager = new GameFilesManager(game.shop, game.objectId);
if (
FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
download.folderName?.endsWith(ext)
)
) {
gameFilesManager.extractDownloadedFile();
} else if (download.folderName) {
gameFilesManager
.extractFilesInDirectory(
path.join(download.downloadPath, download.folderName)
)
.then(() => gameFilesManager.setExtractionComplete());
}
}
private static async processNextQueuedDownload() {
const downloads = await downloadsSublevel
.values()
.all()
.then((games) =>
orderBy(
games.filter((game) => game.status === "paused" && game.queued),
["timestamp"],
["desc"]
)
); );
const [nextItemOnQueue] = downloads; if (WindowManager.mainWindow && download) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(
JSON.stringify({
...status,
game,
})
)
);
}
if (nextItemOnQueue) { const shouldExtract = download.automaticallyExtract;
this.resumeDownload(nextItemOnQueue);
} else { if (progress === 1 && download) {
this.downloadingGameId = null; publishDownloadCompleteNotification(game);
this.usingJsDownloader = false;
this.jsDownloader = null; if (
userPreferences?.seedAfterDownloadComplete &&
download.downloader === Downloader.Torrent
) {
await downloadsSublevel.put(gameId, {
...download,
status: "seeding",
shouldSeed: true,
queued: false,
extracting: shouldExtract,
});
} else {
await downloadsSublevel.put(gameId, {
...download,
status: "complete",
shouldSeed: false,
queued: false,
extracting: shouldExtract,
});
this.cancelDownload(gameId);
}
if (shouldExtract) {
const gameFilesManager = new GameFilesManager(
game.shop,
game.objectId
);
if (
FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
download.folderName?.endsWith(ext)
)
) {
gameFilesManager.extractDownloadedFile();
} else {
gameFilesManager
.extractFilesInDirectory(
path.join(download.downloadPath, download.folderName!)
)
.then(() => {
gameFilesManager.setExtractionComplete();
});
}
}
const downloads = await downloadsSublevel
.values()
.all()
.then((games) => {
return sortBy(
games.filter((game) => game.status === "paused" && game.queued),
"timestamp",
"DESC"
);
});
const [nextItemOnQueue] = downloads;
if (nextItemOnQueue) {
this.resumeDownload(nextItemOnQueue);
} else {
this.downloadingGameId = null;
}
}
} }
} }
@@ -477,17 +248,12 @@ export class DownloadManager {
} }
static async pauseDownload(downloadKey = this.downloadingGameId) { static async pauseDownload(downloadKey = this.downloadingGameId) {
if (this.usingJsDownloader && this.jsDownloader) { await PythonRPC.rpc
logger.log("[DownloadManager] Pausing JS download"); .post("/action", {
this.jsDownloader.pauseDownload(); action: "pause",
} else { game_id: downloadKey,
await PythonRPC.rpc } as PauseDownloadPayload)
.post("/action", { .catch(() => {});
action: "pause",
game_id: downloadKey,
} as PauseDownloadPayload)
.catch(() => {});
}
if (downloadKey === this.downloadingGameId) { if (downloadKey === this.downloadingGameId) {
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@@ -500,25 +266,19 @@ export class DownloadManager {
} }
static async cancelDownload(downloadKey = this.downloadingGameId) { static async cancelDownload(downloadKey = this.downloadingGameId) {
const isActiveDownload = downloadKey === this.downloadingGameId; await PythonRPC.rpc
.post("/action", {
if (isActiveDownload) { action: "cancel",
if (this.usingJsDownloader && this.jsDownloader) { game_id: downloadKey,
logger.log("[DownloadManager] Cancelling JS download"); })
this.jsDownloader.cancelDownload(); .catch((err) => {
this.jsDownloader = null; logger.error("Failed to cancel game download", err);
this.usingJsDownloader = false; });
} else if (!this.isPreparingDownload) {
await PythonRPC.rpc
.post("/action", { action: "cancel", game_id: downloadKey })
.catch((err) => logger.error("Failed to cancel game download", err));
}
if (downloadKey === this.downloadingGameId) {
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
WindowManager.mainWindow?.webContents.send("on-download-progress", null); WindowManager.mainWindow?.webContents.send("on-download-progress", null);
this.downloadingGameId = null; this.downloadingGameId = null;
this.isPreparingDownload = false;
this.usingJsDownloader = false;
} }
} }
@@ -538,241 +298,6 @@ export class DownloadManager {
}); });
} }
private static async getJsDownloadOptions(download: Download): Promise<{
url: string;
savePath: string;
filename?: string;
headers?: Record<string, string>;
} | null> {
const resumingFilename = download.folderName || undefined;
switch (download.downloader) {
case Downloader.Gofile:
return this.getGofileDownloadOptions(download, resumingFilename);
case Downloader.PixelDrain:
return this.getPixelDrainDownloadOptions(download, resumingFilename);
case Downloader.Datanodes:
return this.getDatanodesDownloadOptions(download, resumingFilename);
case Downloader.Buzzheavier:
return this.getBuzzheavierDownloadOptions(download, resumingFilename);
case Downloader.FuckingFast:
return this.getFuckingFastDownloadOptions(download, resumingFilename);
case Downloader.Mediafire:
return this.getMediafireDownloadOptions(download, resumingFilename);
case Downloader.RealDebrid:
return this.getRealDebridDownloadOptions(download, resumingFilename);
case Downloader.TorBox:
return this.getTorBoxDownloadOptions(download, resumingFilename);
case Downloader.Hydra:
return this.getHydraDownloadOptions(download, resumingFilename);
case Downloader.VikingFile:
return this.getVikingFileDownloadOptions(download, resumingFilename);
case Downloader.Rootz:
return this.getRootzDownloadOptions(download, resumingFilename);
default:
return null;
}
}
private static async getGofileDownloadOptions(
download: Download,
resumingFilename?: string
) {
const id = download.uri.split("/").pop();
const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!);
await GofileApi.checkDownloadUrl(downloadLink);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadLink
);
return this.buildDownloadOptions(
downloadLink,
download.downloadPath,
filename,
{ Cookie: `accountToken=${token}` }
);
}
private static async getPixelDrainDownloadOptions(
download: Download,
resumingFilename?: string
) {
const id = download.uri.split("/").pop();
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getDatanodesDownloadOptions(
download: Download,
resumingFilename?: string
) {
const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getBuzzheavierDownloadOptions(
download: Download,
resumingFilename?: string
) {
logger.log(
`[DownloadManager] Processing Buzzheavier download for URI: ${download.uri}`
);
const directUrl = await BuzzheavierApi.getDirectLink(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
directUrl
);
return this.buildDownloadOptions(
directUrl,
download.downloadPath,
filename
);
}
private static async getFuckingFastDownloadOptions(
download: Download,
resumingFilename?: string
) {
logger.log(
`[DownloadManager] Processing FuckingFast download for URI: ${download.uri}`
);
const directUrl = await FuckingFastApi.getDirectLink(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
directUrl
);
return this.buildDownloadOptions(
directUrl,
download.downloadPath,
filename
);
}
private static async getMediafireDownloadOptions(
download: Download,
resumingFilename?: string
) {
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getRealDebridDownloadOptions(
download: Download,
resumingFilename?: string
) {
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getTorBoxDownloadOptions(
download: Download,
resumingFilename?: string
) {
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
if (!url) return null;
return this.buildDownloadOptions(
url,
download.downloadPath,
resumingFilename || name
);
}
private static async getHydraDownloadOptions(
download: Download,
resumingFilename?: string
) {
const downloadUrl = await HydraDebridClient.getDownloadUrl(download.uri);
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getVikingFileDownloadOptions(
download: Download,
resumingFilename?: string
) {
logger.log(
`[DownloadManager] Processing VikingFile download for URI: ${download.uri}`
);
const downloadUrl = await VikingFileApi.getDownloadUrl(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getRootzDownloadOptions(
download: Download,
resumingFilename?: string
) {
const downloadUrl = await RootzApi.getDownloadUrl(download.uri);
const filename = this.resolveFilename(
resumingFilename,
download.uri,
downloadUrl
);
return this.buildDownloadOptions(
downloadUrl,
download.downloadPath,
filename
);
}
private static async getDownloadPayload(download: Download) { private static async getDownloadPayload(download: Download) {
const downloadId = levelKeys.game(download.shop, download.objectId); const downloadId = levelKeys.game(download.shop, download.objectId);
@@ -781,6 +306,7 @@ export class DownloadManager {
const id = download.uri.split("/").pop(); const id = download.uri.split("/").pop();
const token = await GofileApi.authorize(); const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!); const downloadLink = await GofileApi.getDownloadLink(id!);
await GofileApi.checkDownloadUrl(downloadLink); await GofileApi.checkDownloadUrl(downloadLink);
return { return {
@@ -804,6 +330,15 @@ export class DownloadManager {
save_path: download.downloadPath, save_path: download.downloadPath,
}; };
} }
case Downloader.Qiwi: {
const downloadUrl = await QiwiApi.getDownloadUrl(download.uri);
return {
action: "start",
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
};
}
case Downloader.Datanodes: { case Downloader.Datanodes: {
const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri); const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri);
return { return {
@@ -813,50 +348,9 @@ export class DownloadManager {
save_path: download.downloadPath, save_path: download.downloadPath,
}; };
} }
case Downloader.Buzzheavier: {
logger.log(
`[DownloadManager] Processing Buzzheavier download for URI: ${download.uri}`
);
try {
const directUrl = await BuzzheavierApi.getDirectLink(download.uri);
logger.log(`[DownloadManager] Buzzheavier direct URL obtained`);
return this.createDownloadPayload(
directUrl,
download.uri,
downloadId,
download.downloadPath
);
} catch (error) {
logger.error(
`[DownloadManager] Error processing Buzzheavier download:`,
error
);
throw error;
}
}
case Downloader.FuckingFast: {
logger.log(
`[DownloadManager] Processing FuckingFast download for URI: ${download.uri}`
);
try {
const directUrl = await FuckingFastApi.getDirectLink(download.uri);
logger.log(`[DownloadManager] FuckingFast direct URL obtained`);
return this.createDownloadPayload(
directUrl,
download.uri,
downloadId,
download.downloadPath
);
} catch (error) {
logger.error(
`[DownloadManager] Error processing FuckingFast download:`,
error
);
throw error;
}
}
case Downloader.Mediafire: { case Downloader.Mediafire: {
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri); const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
return { return {
action: "start", action: "start",
game_id: downloadId, game_id: downloadId,
@@ -873,6 +367,7 @@ export class DownloadManager {
}; };
case Downloader.RealDebrid: { case Downloader.RealDebrid: {
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri); const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid); if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid);
return { return {
@@ -885,6 +380,7 @@ export class DownloadManager {
} }
case Downloader.TorBox: { case Downloader.TorBox: {
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri); const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
if (!url) return; if (!url) return;
return { return {
action: "start", action: "start",
@@ -899,6 +395,7 @@ export class DownloadManager {
const downloadUrl = await HydraDebridClient.getDownloadUrl( const downloadUrl = await HydraDebridClient.getDownloadUrl(
download.uri download.uri
); );
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra); if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra);
return { return {
@@ -909,89 +406,12 @@ export class DownloadManager {
allow_multiple_connections: true, allow_multiple_connections: true,
}; };
} }
case Downloader.VikingFile: {
logger.log(
`[DownloadManager] Processing VikingFile download for URI: ${download.uri}`
);
const downloadUrl = await VikingFileApi.getDownloadUrl(download.uri);
return this.createDownloadPayload(
downloadUrl,
download.uri,
downloadId,
download.downloadPath
);
}
case Downloader.Rootz: {
const downloadUrl = await RootzApi.getDownloadUrl(download.uri);
return {
action: "start",
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
};
}
default:
return undefined;
}
}
static async validateDownloadUrl(download: Download): Promise<void> {
const useJsDownloader = await this.shouldUseJsDownloader();
const isHttp = this.isHttpDownloader(download.downloader);
if (useJsDownloader && isHttp) {
const options = await this.getJsDownloadOptions(download);
if (!options) {
throw new Error("Failed to validate download URL");
}
} else if (isHttp) {
await this.getDownloadPayload(download);
} }
} }
static async startDownload(download: Download) { static async startDownload(download: Download) {
const useJsDownloader = await this.shouldUseJsDownloader(); const payload = await this.getDownloadPayload(download);
const isHttp = this.isHttpDownloader(download.downloader); await PythonRPC.rpc.post("/action", payload);
const downloadId = levelKeys.game(download.shop, download.objectId); this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
if (useJsDownloader && isHttp) {
logger.log("[DownloadManager] Using JS HTTP downloader");
// Set preparing state immediately so UI knows download is starting
this.downloadingGameId = downloadId;
this.isPreparingDownload = true;
this.usingJsDownloader = true;
try {
const options = await this.getJsDownloadOptions(download);
if (!options) {
this.isPreparingDownload = false;
this.usingJsDownloader = false;
this.downloadingGameId = null;
throw new Error("Failed to get download options for JS downloader");
}
this.jsDownloader = new JsHttpDownloader();
this.isPreparingDownload = false;
this.jsDownloader.startDownload(options).catch((err) => {
logger.error("[DownloadManager] JS download error:", err);
this.usingJsDownloader = false;
this.jsDownloader = null;
});
} catch (err) {
this.isPreparingDownload = false;
this.usingJsDownloader = false;
this.downloadingGameId = null;
throw err;
}
} else {
logger.log("[DownloadManager] Using Python RPC downloader");
const payload = await this.getDownloadPayload(download);
await PythonRPC.rpc.post("/action", payload);
this.downloadingGameId = downloadId;
this.usingJsDownloader = false;
}
} }
} }

View File

@@ -1,4 +1,3 @@
export * from "./download-manager"; export * from "./download-manager";
export * from "./real-debrid"; export * from "./real-debrid";
export * from "./torbox"; export * from "./torbox";
export * from "./js-http-downloader";

View File

@@ -1,387 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { logger } from "../logger";
export interface JsHttpDownloaderStatus {
folderName: string;
fileSize: number;
progress: number;
downloadSpeed: number;
numPeers: number;
numSeeds: number;
status: "active" | "paused" | "complete" | "error";
bytesDownloaded: number;
}
export interface JsHttpDownloaderOptions {
url: string;
savePath: string;
filename?: string;
headers?: Record<string, string>;
}
export class JsHttpDownloader {
private abortController: AbortController | null = null;
private writeStream: fs.WriteStream | null = null;
private currentOptions: JsHttpDownloaderOptions | null = null;
private bytesDownloaded = 0;
private fileSize = 0;
private downloadSpeed = 0;
private status: "active" | "paused" | "complete" | "error" = "paused";
private folderName = "";
private lastSpeedUpdate = Date.now();
private bytesAtLastSpeedUpdate = 0;
private isDownloading = false;
async startDownload(options: JsHttpDownloaderOptions): Promise<void> {
if (this.isDownloading) {
logger.log(
"[JsHttpDownloader] Download already in progress, resuming..."
);
return this.resumeDownload();
}
this.currentOptions = options;
this.abortController = new AbortController();
this.status = "active";
this.isDownloading = true;
const { url, savePath, filename, headers = {} } = options;
const { filePath, startByte, usedFallback } = this.prepareDownloadPath(
savePath,
filename,
url
);
const requestHeaders = this.buildRequestHeaders(headers, startByte);
try {
await this.executeDownload(
url,
requestHeaders,
filePath,
startByte,
savePath,
usedFallback
);
} catch (err) {
this.handleDownloadError(err as Error);
} finally {
this.isDownloading = false;
this.cleanup();
}
}
private prepareDownloadPath(
savePath: string,
filename: string | undefined,
url: string
): { filePath: string; startByte: number; usedFallback: boolean } {
const extractedFilename = filename || this.extractFilename(url);
const usedFallback = !extractedFilename;
const resolvedFilename = extractedFilename || "download";
this.folderName = resolvedFilename;
const filePath = path.join(savePath, resolvedFilename);
if (!fs.existsSync(savePath)) {
fs.mkdirSync(savePath, { recursive: true });
}
let startByte = 0;
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
startByte = stats.size;
this.bytesDownloaded = startByte;
logger.log(`[JsHttpDownloader] Resuming download from byte ${startByte}`);
}
this.resetSpeedTracking();
return { filePath, startByte, usedFallback };
}
private buildRequestHeaders(
headers: Record<string, string>,
startByte: number
): Record<string, string> {
const requestHeaders: Record<string, string> = { ...headers };
if (startByte > 0) {
requestHeaders["Range"] = `bytes=${startByte}-`;
}
return requestHeaders;
}
private resetSpeedTracking(): void {
this.lastSpeedUpdate = Date.now();
this.bytesAtLastSpeedUpdate = this.bytesDownloaded;
this.downloadSpeed = 0;
}
private parseFileSize(response: Response, startByte: number): void {
const contentRange = response.headers.get("content-range");
if (contentRange) {
const match = /bytes \d+-\d+\/(\d+)/.exec(contentRange);
if (match) {
this.fileSize = Number.parseInt(match[1], 10);
}
return;
}
const contentLength = response.headers.get("content-length");
if (contentLength) {
this.fileSize = startByte + Number.parseInt(contentLength, 10);
}
}
private async executeDownload(
url: string,
requestHeaders: Record<string, string>,
filePath: string,
startByte: number,
savePath: string,
usedFallback: boolean
): Promise<void> {
const response = await fetch(url, {
headers: requestHeaders,
signal: this.abortController?.signal,
});
// Handle 416 Range Not Satisfiable - existing file is larger than server file
// This happens when downloading same game from different source
if (response.status === 416 && startByte > 0) {
logger.log(
"[JsHttpDownloader] Range not satisfiable, deleting existing file and restarting"
);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
this.bytesDownloaded = 0;
this.resetSpeedTracking();
// Retry without Range header
const headersWithoutRange = { ...requestHeaders };
delete headersWithoutRange["Range"];
return this.executeDownload(
url,
headersWithoutRange,
filePath,
0,
savePath,
usedFallback
);
}
if (!response.ok && response.status !== 206) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.parseFileSize(response, startByte);
// If we used "download" fallback, try to get filename from Content-Disposition
let actualFilePath = filePath;
if (usedFallback && startByte === 0) {
const headerFilename = this.parseContentDisposition(response);
if (headerFilename) {
actualFilePath = path.join(savePath, headerFilename);
this.folderName = headerFilename;
logger.log(
`[JsHttpDownloader] Using filename from Content-Disposition: ${headerFilename}`
);
}
}
if (!response.body) {
throw new Error("Response body is null");
}
const flags = startByte > 0 ? "a" : "w";
this.writeStream = fs.createWriteStream(actualFilePath, { flags });
const readableStream = this.createReadableStream(response.body.getReader());
await pipeline(readableStream, this.writeStream);
this.status = "complete";
this.downloadSpeed = 0;
logger.log("[JsHttpDownloader] Download complete");
}
private parseContentDisposition(response: Response): string | undefined {
const header = response.headers.get("content-disposition");
if (!header) return undefined;
// Try to extract filename from Content-Disposition header
// Formats: attachment; filename="file.zip" or attachment; filename=file.zip
const filenameMatch = /filename\*?=['"]?(?:UTF-8'')?([^"';\n]+)['"]?/i.exec(
header
);
if (filenameMatch?.[1]) {
try {
return decodeURIComponent(filenameMatch[1].trim());
} catch {
return filenameMatch[1].trim();
}
}
return undefined;
}
private createReadableStream(
reader: ReadableStreamDefaultReader<Uint8Array>
): Readable {
const onChunk = (length: number) => {
this.bytesDownloaded += length;
this.updateSpeed();
};
return new Readable({
read() {
reader
.read()
.then(({ done, value }) => {
if (done) {
this.push(null);
return;
}
onChunk(value.length);
this.push(Buffer.from(value));
})
.catch((err: Error) => {
if (err.name === "AbortError") {
this.push(null);
} else {
this.destroy(err);
}
});
},
});
}
private handleDownloadError(err: Error): void {
// Handle abort/cancellation errors - these are expected when user pauses/cancels
if (
err.name === "AbortError" ||
(err as NodeJS.ErrnoException).code === "ERR_STREAM_PREMATURE_CLOSE"
) {
logger.log("[JsHttpDownloader] Download aborted");
this.status = "paused";
} else {
logger.error("[JsHttpDownloader] Download error:", err);
this.status = "error";
throw err;
}
}
private async resumeDownload(): Promise<void> {
if (!this.currentOptions) {
throw new Error("No download options available for resume");
}
this.isDownloading = false;
await this.startDownload(this.currentOptions);
}
pauseDownload(): void {
if (this.abortController) {
logger.log("[JsHttpDownloader] Pausing download");
this.abortController.abort();
this.status = "paused";
this.downloadSpeed = 0;
}
}
cancelDownload(deleteFile = true): void {
if (this.abortController) {
logger.log("[JsHttpDownloader] Cancelling download");
this.abortController.abort();
}
this.cleanup();
if (deleteFile && this.currentOptions && this.status !== "complete") {
const filePath = path.join(this.currentOptions.savePath, this.folderName);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
logger.log("[JsHttpDownloader] Deleted partial file");
} catch (err) {
logger.error(
"[JsHttpDownloader] Failed to delete partial file:",
err
);
}
}
}
this.reset();
}
getDownloadStatus(): JsHttpDownloaderStatus | null {
if (!this.currentOptions && this.status !== "active") {
return null;
}
let progress = 0;
if (this.status === "complete") {
progress = 1;
} else if (this.fileSize > 0) {
progress = this.bytesDownloaded / this.fileSize;
}
return {
folderName: this.folderName,
fileSize: this.fileSize,
progress,
downloadSpeed: this.downloadSpeed,
numPeers: 0,
numSeeds: 0,
status: this.status,
bytesDownloaded: this.bytesDownloaded,
};
}
private updateSpeed(): void {
const now = Date.now();
const elapsed = (now - this.lastSpeedUpdate) / 1000;
if (elapsed >= 1) {
const bytesDelta = this.bytesDownloaded - this.bytesAtLastSpeedUpdate;
this.downloadSpeed = bytesDelta / elapsed;
this.lastSpeedUpdate = now;
this.bytesAtLastSpeedUpdate = this.bytesDownloaded;
}
}
private extractFilename(url: string): string | undefined {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const pathParts = pathname.split("/");
const filename = pathParts.at(-1);
if (filename?.includes(".") && filename.length > 0) {
return decodeURIComponent(filename);
}
} catch {
// Invalid URL
}
return undefined;
}
private cleanup(): void {
if (this.writeStream) {
this.writeStream.close();
this.writeStream = null;
}
this.abortController = null;
}
private reset(): void {
this.currentOptions = null;
this.bytesDownloaded = 0;
this.fileSize = 0;
this.downloadSpeed = 0;
this.status = "paused";
this.folderName = "";
this.isDownloading = false;
}
}

View File

@@ -1,13 +0,0 @@
import { gameExecutables } from "./process-watcher";
export class GameExecutables {
static getExecutablesForGame(objectId: string): string[] | null {
const executables = gameExecutables[objectId];
if (!executables || executables.length === 0) {
return null;
}
return executables.map((exe) => exe.exe);
}
}

View File

@@ -3,59 +3,24 @@ import fs from "node:fs";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared"; import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
import { SevenZip, ExtractionProgress } from "./7zip"; import { SevenZip } from "./7zip";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { publishExtractionCompleteNotification } from "./notifications"; import { publishExtractionCompleteNotification } from "./notifications";
import { logger } from "./logger"; import { logger } from "./logger";
import { GameExecutables } from "./game-executables";
const PROGRESS_THROTTLE_MS = 1000;
export class GameFilesManager { export class GameFilesManager {
private lastProgressUpdate = 0;
constructor( constructor(
private readonly shop: GameShop, private readonly shop: GameShop,
private readonly objectId: string private readonly objectId: string
) {} ) {}
private get gameKey() {
return levelKeys.game(this.shop, this.objectId);
}
private async updateExtractionProgress(progress: number, force = false) {
const now = Date.now();
if (!force && now - this.lastProgressUpdate < PROGRESS_THROTTLE_MS) {
return;
}
this.lastProgressUpdate = now;
const download = await downloadsSublevel.get(this.gameKey);
if (!download) return;
await downloadsSublevel.put(this.gameKey, {
...download,
extractionProgress: progress,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-progress",
this.shop,
this.objectId,
progress
);
}
private async clearExtractionState() { private async clearExtractionState() {
const download = await downloadsSublevel.get(this.gameKey); const gameKey = levelKeys.game(this.shop, this.objectId);
if (!download) return; const download = await downloadsSublevel.get(gameKey);
await downloadsSublevel.put(this.gameKey, { await downloadsSublevel.put(gameKey, {
...download, ...download!,
extracting: false, extracting: false,
extractionProgress: 0,
}); });
WindowManager.mainWindow?.webContents.send( WindowManager.mainWindow?.webContents.send(
@@ -65,10 +30,6 @@ export class GameFilesManager {
); );
} }
private readonly handleProgress = (progress: ExtractionProgress) => {
this.updateExtractionProgress(progress.percent / 100);
};
async extractFilesInDirectory(directoryPath: string) { async extractFilesInDirectory(directoryPath: string) {
if (!fs.existsSync(directoryPath)) return; if (!fs.existsSync(directoryPath)) return;
const files = await fs.promises.readdir(directoryPath); const files = await fs.promises.readdir(directoryPath);
@@ -81,66 +42,53 @@ export class GameFilesManager {
(file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file) (file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
); );
if (filesToExtract.length === 0) return; await Promise.all(
filesToExtract.map((file) => {
await this.updateExtractionProgress(0, true); return new Promise((resolve, reject) => {
SevenZip.extractFile(
const totalFiles = filesToExtract.length; {
let completedFiles = 0; filePath: path.join(directoryPath, file),
cwd: directoryPath,
for (const file of filesToExtract) { passwords: ["online-fix.me", "steamrip.com"],
try { },
const result = await SevenZip.extractFile( () => {
{ resolve(true);
filePath: path.join(directoryPath, file), },
cwd: directoryPath, () => {
passwords: ["online-fix.me", "steamrip.com"], reject(new Error(`Failed to extract file: ${file}`));
}, this.clearExtractionState();
(progress) => { }
const overallProgress =
(completedFiles + progress.percent / 100) / totalFiles;
this.updateExtractionProgress(overallProgress);
}
);
if (result.success) {
completedFiles++;
await this.updateExtractionProgress(
completedFiles / totalFiles,
true
); );
} });
} catch (err) { })
logger.error(`Failed to extract file: ${file}`, err); );
await this.clearExtractionState();
return; compressedFiles.forEach((file) => {
const extractionPath = path.join(directoryPath, file);
if (fs.existsSync(extractionPath)) {
fs.unlink(extractionPath, (err) => {
if (err) {
logger.error(`Failed to delete file: ${file}`, err);
this.clearExtractionState();
}
});
} }
} });
const archivePaths = compressedFiles
.map((file) => path.join(directoryPath, file))
.filter((archivePath) => fs.existsSync(archivePath));
if (archivePaths.length > 0) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
archivePaths
);
}
} }
async setExtractionComplete(publishNotification = true) { async setExtractionComplete(publishNotification = true) {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([ const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey), downloadsSublevel.get(gameKey),
gamesSublevel.get(this.gameKey), gamesSublevel.get(gameKey),
]); ]);
if (!download) return; await downloadsSublevel.put(gameKey, {
...download!,
await downloadsSublevel.put(this.gameKey, {
...download,
extracting: false, extracting: false,
extractionProgress: 0,
}); });
WindowManager.mainWindow?.webContents.send( WindowManager.mainWindow?.webContents.send(
@@ -149,109 +97,17 @@ export class GameFilesManager {
this.objectId this.objectId
); );
if (publishNotification && game) { if (publishNotification) {
publishExtractionCompleteNotification(game); publishExtractionCompleteNotification(game!);
} }
await this.searchAndBindExecutable();
}
async searchAndBindExecutable(): Promise<void> {
try {
const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey),
gamesSublevel.get(this.gameKey),
]);
if (!download || !game || game.executablePath) {
return;
}
const executableNames = GameExecutables.getExecutablesForGame(
this.objectId
);
if (!executableNames || executableNames.length === 0) {
return;
}
if (!download.folderName) {
return;
}
const gameFolderPath = path.join(
download.downloadPath,
download.folderName
);
if (!fs.existsSync(gameFolderPath)) {
return;
}
const foundExePath = await this.findExecutableInFolder(
gameFolderPath,
executableNames
);
if (foundExePath) {
logger.info(
`[GameFilesManager] Auto-detected executable for ${this.objectId}: ${foundExePath}`
);
await gamesSublevel.put(this.gameKey, {
...game,
executablePath: foundExePath,
});
WindowManager.mainWindow?.webContents.send("on-library-batch-complete");
}
} catch (err) {
logger.error(
`[GameFilesManager] Error searching for executable: ${this.objectId}`,
err
);
}
}
private async findExecutableInFolder(
folderPath: string,
executableNames: string[]
): Promise<string | null> {
const normalizedNames = new Set(
executableNames.map((name) => name.toLowerCase())
);
try {
const entries = await fs.promises.readdir(folderPath, {
withFileTypes: true,
recursive: true,
});
for (const entry of entries) {
if (!entry.isFile()) continue;
const fileName = entry.name.toLowerCase();
if (normalizedNames.has(fileName)) {
const parentPath =
"parentPath" in entry
? entry.parentPath
: (entry as unknown as { path?: string }).path || folderPath;
return path.join(parentPath, entry.name);
}
}
} catch {
// Silently fail if folder cannot be read
}
return null;
} }
async extractDownloadedFile() { async extractDownloadedFile() {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([ const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey), downloadsSublevel.get(gameKey),
gamesSublevel.get(this.gameKey), gamesSublevel.get(gameKey),
]); ]);
if (!download || !game) return false; if (!download || !game) return false;
@@ -263,39 +119,39 @@ export class GameFilesManager {
path.parse(download.folderName!).name path.parse(download.folderName!).name
); );
await this.updateExtractionProgress(0, true); SevenZip.extractFile(
{
try { filePath,
const result = await SevenZip.extractFile( outputPath: extractionPath,
{ passwords: ["online-fix.me", "steamrip.com"],
filePath, },
outputPath: extractionPath, async () => {
passwords: ["online-fix.me", "steamrip.com"],
},
this.handleProgress
);
if (result.success) {
await this.extractFilesInDirectory(extractionPath); await this.extractFilesInDirectory(extractionPath);
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) { if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
WindowManager.mainWindow?.webContents.send( fs.unlink(filePath, (err) => {
"on-archive-deletion-prompt", if (err) {
[filePath] logger.error(
); `Failed to delete file: ${download.folderName}`,
err
);
this.clearExtractionState();
}
});
} }
await downloadsSublevel.put(this.gameKey, { await downloadsSublevel.put(gameKey, {
...download, ...download!,
folderName: path.parse(download.folderName!).name, folderName: path.parse(download.folderName!).name,
}); });
await this.setExtractionComplete(); this.setExtractionComplete();
},
() => {
this.clearExtractionState();
} }
} catch (err) { );
logger.error(`Failed to extract downloaded file: ${filePath}`, err);
await this.clearExtractionState();
}
return true; return true;
} }

Some files were not shown because too many files have changed in this diff Show More