mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Compare commits
92 Commits
v3.1.4
...
fix/using-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
decacfa1d9 | ||
|
|
7c7f621d95 | ||
|
|
032293b339 | ||
|
|
f6eedde976 | ||
|
|
18c6994df6 | ||
|
|
239b29687c | ||
|
|
dfc2dd1c8b | ||
|
|
954037b826 | ||
|
|
8a30e946e3 | ||
|
|
cd871ec359 | ||
|
|
548b7c3f41 | ||
|
|
bbcdb42708 | ||
|
|
e9d541498e | ||
|
|
4e34f41ee0 | ||
|
|
049c27cdb7 | ||
|
|
2ba653429f | ||
|
|
9574e39d75 | ||
|
|
8ebb5edfbc | ||
|
|
69787ee068 | ||
|
|
d1fa4895e4 | ||
|
|
5d0e825880 | ||
|
|
b06339d362 | ||
|
|
3d0bf11359 | ||
|
|
2346a5bf86 | ||
|
|
81cb73c243 | ||
|
|
923f7d7e80 | ||
|
|
153ab05174 | ||
|
|
ff0ef74066 | ||
|
|
bc2ee2dc4c | ||
|
|
d866face54 | ||
|
|
44fd971c95 | ||
|
|
9941460c60 | ||
|
|
15f721ac39 | ||
|
|
56fabb2881 | ||
|
|
ffd3e37b48 | ||
|
|
c4378c0ffc | ||
|
|
af4fcb8f06 | ||
|
|
09c1170407 | ||
|
|
89f1ce5ead | ||
|
|
392279c4e1 | ||
|
|
8fbe23e61c | ||
|
|
d4be5b8c66 | ||
|
|
21a88b889f | ||
|
|
a0a3697516 | ||
|
|
317434f663 | ||
|
|
cac2a7a70e | ||
|
|
11700b7c16 | ||
|
|
2407be0fb2 | ||
|
|
2a31c32cda | ||
|
|
7a7f270482 | ||
|
|
ab2d8c351b | ||
|
|
87acdea5ab | ||
|
|
385db5c936 | ||
|
|
cade56bb12 | ||
|
|
3efb1425b9 | ||
|
|
2df57b071d | ||
|
|
5061695500 | ||
|
|
190ddeb46e | ||
|
|
e6d76a5dbe | ||
|
|
2ddda4e4d2 | ||
|
|
ef3bf98903 | ||
|
|
b68fe300ba | ||
|
|
29ba0cca85 | ||
|
|
82b1d710c2 | ||
|
|
93b86f8c6c | ||
|
|
8cf549ff05 | ||
|
|
257a71d626 | ||
|
|
f3d617a13a | ||
|
|
9672e649e4 | ||
|
|
e2f798c627 | ||
|
|
52c159fe51 | ||
|
|
9849fbb31c | ||
|
|
addc2a74d3 | ||
|
|
10766526c5 | ||
|
|
bfdc2787d4 | ||
|
|
c60cd4bee4 | ||
|
|
59bc23bbd8 | ||
|
|
16b50fc22b | ||
|
|
720a7aa2a0 | ||
|
|
5c2bafcfe8 | ||
|
|
c30c685ee4 | ||
|
|
fba86002d1 | ||
|
|
a121ef77c0 | ||
|
|
bd653be071 | ||
|
|
297ca5a190 | ||
|
|
6278600b98 | ||
|
|
1226483deb | ||
|
|
0661cbd661 | ||
|
|
ad204e3879 | ||
|
|
afcfcbf482 | ||
|
|
ac6eb247df | ||
|
|
47a5f4d327 |
@@ -125,6 +125,10 @@ cd hydra
|
||||
yarn
|
||||
```
|
||||
|
||||
### <a name="install-openssl-11"></a> Instale OpenSSL 1.1
|
||||
|
||||
[OpenSSL 1.1](https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe) é exigido pelo libtorrent em ambientes Windows.
|
||||
|
||||
### <a name="install-python-39"></a> Instale Python 3.9
|
||||
|
||||
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3913/).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.1.4",
|
||||
"version": "3.1.5",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -47,13 +47,13 @@
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
"color.js": "^1.2.0",
|
||||
"create-desktop-shortcuts": "^1.11.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dexie": "^4.0.10",
|
||||
"diskusage": "^1.2.0",
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-updater": "^6.3.9",
|
||||
"file-type": "^19.6.0",
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import sys, json, urllib.parse, psutil
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import json
|
||||
import urllib.parse
|
||||
import sys
|
||||
import psutil
|
||||
from torrent_downloader import TorrentDownloader
|
||||
from http_downloader import HttpDownloader
|
||||
from profile_image_processor import ProfileImageProcessor
|
||||
import libtorrent as lt
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Retrieve command line arguments
|
||||
torrent_port = sys.argv[1]
|
||||
http_port = sys.argv[2]
|
||||
http_port = int(sys.argv[2])
|
||||
rpc_password = sys.argv[3]
|
||||
start_download_payload = sys.argv[4]
|
||||
start_seeding_payload = sys.argv[5]
|
||||
|
||||
downloads = {}
|
||||
# This can be streamed down from Node
|
||||
downloading_game_id = -1
|
||||
|
||||
torrent_session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=torrent_port)})
|
||||
torrent_session = lt.session({'listen_interfaces': f'0.0.0.0:{torrent_port}'})
|
||||
|
||||
if start_download_payload:
|
||||
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
||||
downloading_game_id = initial_download['game_id']
|
||||
|
||||
|
||||
if initial_download['url'].startswith('magnet'):
|
||||
torrent_downloader = TorrentDownloader(torrent_session)
|
||||
downloads[initial_download['game_id']] = torrent_downloader
|
||||
@@ -49,135 +49,142 @@ if start_seeding_payload:
|
||||
except Exception as e:
|
||||
print("Error starting seeding", e)
|
||||
|
||||
def validate_rpc_password():
|
||||
"""Middleware to validate RPC password."""
|
||||
header_password = request.headers.get('x-hydra-rpc-password')
|
||||
if header_password != rpc_password:
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
class RequestHandler(BaseHTTPRequestHandler):
|
||||
def validate_rpc_password(self):
|
||||
header_password = self.headers.get('x-hydra-rpc-password')
|
||||
if header_password != rpc_password:
|
||||
self.send_response(401)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": "Unauthorized"}).encode('utf-8'))
|
||||
return False
|
||||
return True
|
||||
|
||||
@app.route("/status", methods=["GET"])
|
||||
def status():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
def do_GET(self):
|
||||
if self.path == "/status":
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
downloader = downloads.get(downloading_game_id)
|
||||
if downloader:
|
||||
status = downloads.get(downloading_game_id).get_download_status()
|
||||
return jsonify(status), 200
|
||||
else:
|
||||
return jsonify(None)
|
||||
|
||||
@app.route("/seed-status", methods=["GET"])
|
||||
def seed_status():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
seed_status = []
|
||||
|
||||
for game_id, downloader in downloads.items():
|
||||
if not downloader:
|
||||
continue
|
||||
|
||||
response = downloader.get_download_status()
|
||||
if response is None:
|
||||
continue
|
||||
|
||||
if response.get('status') == 5:
|
||||
seed_status.append({
|
||||
'gameId': game_id,
|
||||
**response,
|
||||
})
|
||||
|
||||
return jsonify(seed_status), 200
|
||||
|
||||
@app.route("/healthcheck", methods=["GET"])
|
||||
def healthcheck():
|
||||
return "", 200
|
||||
|
||||
@app.route("/process-list", methods=["GET"])
|
||||
def process_list():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'name'])]
|
||||
return jsonify(process_list), 200
|
||||
|
||||
@app.route("/profile-image", methods=["POST"])
|
||||
def profile_image():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
data = request.get_json()
|
||||
image_path = data.get('image_path')
|
||||
|
||||
try:
|
||||
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
|
||||
return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
@app.route("/action", methods=["POST"])
|
||||
def action():
|
||||
global torrent_session
|
||||
global downloading_game_id
|
||||
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
data = request.get_json()
|
||||
action = data.get('action')
|
||||
game_id = data.get('game_id')
|
||||
|
||||
if action == 'start':
|
||||
url = data.get('url')
|
||||
|
||||
existing_downloader = downloads.get(game_id)
|
||||
|
||||
if url.startswith('magnet'):
|
||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], "")
|
||||
downloader = downloads.get(downloading_game_id)
|
||||
if downloader:
|
||||
status = downloader.get_download_status()
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
||||
else:
|
||||
torrent_downloader = TorrentDownloader(torrent_session)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(None).encode('utf-8'))
|
||||
|
||||
elif self.path == "/seed-status":
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
seed_status = []
|
||||
for game_id, downloader in downloads.items():
|
||||
if not downloader:
|
||||
continue
|
||||
|
||||
response = downloader.get_download_status()
|
||||
if response is None:
|
||||
continue
|
||||
|
||||
if response.get('status') == 5:
|
||||
seed_status.append({
|
||||
'gameId': game_id,
|
||||
**response,
|
||||
})
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(seed_status).encode('utf-8'))
|
||||
|
||||
elif self.path == "/healthcheck":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
elif self.path == "/process-list":
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'name'])]
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(process_list).encode('utf-8'))
|
||||
|
||||
def do_POST(self):
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length)
|
||||
data = json.loads(post_data.decode('utf-8'))
|
||||
|
||||
if self.path == "/profile-image":
|
||||
image_path = data.get('image_path')
|
||||
try:
|
||||
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'imagePath': processed_image_path, 'mimeType': mime_type}).encode('utf-8'))
|
||||
except Exception as e:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
||||
|
||||
elif self.path == "/action":
|
||||
global downloading_game_id
|
||||
action = data.get('action')
|
||||
game_id = data.get('game_id')
|
||||
|
||||
if action == 'start':
|
||||
url = data.get('url')
|
||||
|
||||
existing_downloader = downloads.get(game_id)
|
||||
|
||||
if url.startswith('magnet'):
|
||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], "")
|
||||
else:
|
||||
torrent_downloader = TorrentDownloader(torrent_session)
|
||||
downloads[game_id] = torrent_downloader
|
||||
torrent_downloader.start_download(url, data['save_path'], "")
|
||||
else:
|
||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
else:
|
||||
http_downloader = HttpDownloader()
|
||||
downloads[game_id] = http_downloader
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
|
||||
downloading_game_id = game_id
|
||||
|
||||
elif action == 'pause':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.pause_download()
|
||||
downloading_game_id = -1
|
||||
elif action == 'cancel':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
elif action == 'resume_seeding':
|
||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||
downloads[game_id] = torrent_downloader
|
||||
torrent_downloader.start_download(url, data['save_path'], "")
|
||||
else:
|
||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
torrent_downloader.start_download(data['url'], data['save_path'], "")
|
||||
elif action == 'pause_seeding':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
else:
|
||||
http_downloader = HttpDownloader()
|
||||
downloads[game_id] = http_downloader
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
|
||||
downloading_game_id = game_id
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": "Invalid action"}).encode('utf-8'))
|
||||
return
|
||||
|
||||
elif action == 'pause':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.pause_download()
|
||||
downloading_game_id = -1
|
||||
elif action == 'cancel':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
elif action == 'resume_seeding':
|
||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||
downloads[game_id] = torrent_downloader
|
||||
torrent_downloader.start_download(data['url'], data['save_path'], "")
|
||||
elif action == 'pause_seeding':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
|
||||
else:
|
||||
return jsonify({"error": "Invalid action"}), 400
|
||||
|
||||
return "", 200
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=int(http_port))
|
||||
|
||||
server = HTTPServer(('0.0.0.0', http_port), RequestHandler)
|
||||
print(f"Server running on port {http_port}")
|
||||
server.serve_forever()
|
||||
|
||||
@@ -4,5 +4,4 @@ cx_Logging; sys_platform == 'win32'
|
||||
pywin32; sys_platform == 'win32'
|
||||
psutil
|
||||
Pillow
|
||||
flask
|
||||
aria2p
|
||||
|
||||
@@ -49,14 +49,14 @@ fs.readdir(dist, async (err, files) => {
|
||||
})
|
||||
);
|
||||
|
||||
if (uploads.length > 0) {
|
||||
for (const upload of uploads) {
|
||||
await fetch(process.env.BUILD_WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uploads,
|
||||
upload,
|
||||
branchName: process.env.BRANCH_NAME,
|
||||
version: packageJson.version,
|
||||
githubActor: process.env.GITHUB_ACTOR,
|
||||
|
||||
@@ -1,292 +1,316 @@
|
||||
{
|
||||
"language_name": "اَلْعَرَبِيَّةُ",
|
||||
"language_name": "العربية",
|
||||
"app": {
|
||||
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
||||
},
|
||||
"home": {
|
||||
"featured": "مميّز",
|
||||
"surprise_me": "فاجئني",
|
||||
"featured": "مميز",
|
||||
"surprise_me": "مفاجئني",
|
||||
"no_results": "لم يتم العثور على نتائج",
|
||||
"start_typing": "بدء الكتابة للبحث...",
|
||||
"hot": "الأكثر رواجا الآن",
|
||||
"start_typing": "ابدأ الكتابة للبحث...",
|
||||
"hot": "الأكثر شيوعًا الآن",
|
||||
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||
"achievements": "🏆 ألعاب للتغلب عليها"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "قائمة الألعاب",
|
||||
"catalogue": "الكـتالوج",
|
||||
"downloads": "التنزيلات",
|
||||
"settings": "إعدادات",
|
||||
"settings": "الإعدادات",
|
||||
"my_library": "مكتبتي",
|
||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||
"paused": "{{title}} (متوقف مؤقتًا)",
|
||||
"paused": "{{title}} (معلّق)",
|
||||
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||
"filter": "بحث في المكتبة",
|
||||
"filter": "تصفية المكتبة",
|
||||
"home": "الرئيسية",
|
||||
"queued": "{{title}} (في قائمة الانتظار)",
|
||||
"game_has_no_executable": "لم يتم تحديد اللعبة القابلة للتنفيذ",
|
||||
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
||||
"sign_in": "تسجيل الدخول",
|
||||
"friends": "أصدقاء",
|
||||
"need_help": "بحاجة الى مساعدة؟"
|
||||
"friends": "الأصدقاء",
|
||||
"need_help": "تحتاج مساعدة؟"
|
||||
},
|
||||
"header": {
|
||||
"search": "ابحث عن الألعاب",
|
||||
"home": "الرئيسية",
|
||||
"catalogue": "قائمة الألعاب",
|
||||
"catalogue": "الكـتالوج",
|
||||
"downloads": "التنزيلات",
|
||||
"search_results": "نتائج البحث",
|
||||
"settings": "إعدادات",
|
||||
"version_available_install": "إصدار {{version}} متاح. ",
|
||||
"version_available_download": "إصدار {{version}} متاح. "
|
||||
"settings": "الإعدادات",
|
||||
"version_available_install": "الإصدار {{version}} متوفر. انقر هنا لإعادة التشغيل والتثبيت.",
|
||||
"version_available_download": "الإصدار {{version}} متوفر. انقر هنا للتنزيل."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||
"downloading_metadata": "جارٍ التنزيل {{title}} البيانات الوصفية...",
|
||||
"downloading": "جارٍ التنزيل {{title}}… ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}",
|
||||
"calculating_eta": "جارٍ التنزيل {{title}}… ({{percentage}} مكتمل) - حساب الوقت المتبقي...",
|
||||
"checking_files": "التحقق {{title}} ملفات…({{percentage}} مكتمل)"
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية لـ {{title}}...",
|
||||
"downloading": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||
"calculating_eta": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - جاري حساب الوقت المتبقي...",
|
||||
"checking_files": "جارٍ فحص ملفات {{title}}... ({{percentage}} اكتمال)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "الصفحة التالية",
|
||||
"previous_page": "الصفحة السابقة"
|
||||
"search": "تصفية...",
|
||||
"developers": "المطورون",
|
||||
"genres": "الأنواع",
|
||||
"tags": "العلامات",
|
||||
"publishers": "الناشرون",
|
||||
"download_sources": "مصادر التنزيل",
|
||||
"result_count": "{{resultCount}} نتيجة",
|
||||
"filter_count": "{{filterCount}} متاح",
|
||||
"clear_filters": "مسح {{filterCount}} المحددة"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "افتح خيارات التنزيل",
|
||||
"download_options_zero": "{{count}} خيارات التنزيل",
|
||||
"updated_at": "تم التحديث {{updated_at}}",
|
||||
"install": "ثَبَّتَ",
|
||||
"open_download_options": "فتح خيارات التنزيل",
|
||||
"download_options_zero": "لا توجد خيارات تنزيل",
|
||||
"download_options_one": "خيار تنزيل واحد",
|
||||
"download_options_other": "{{count}} خيارات تنزيل",
|
||||
"updated_at": "تم التحديث في {{updated_at}}",
|
||||
"install": "تثبيت",
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"cancel": "إلغاء",
|
||||
"remove": "إزالة",
|
||||
"space_left_on_disk": "{{space}} متبقية على القرص",
|
||||
"eta": "الوقت المتبقي {{eta}}",
|
||||
"calculating_eta": "جارٍ حساب الوقت المتبقي…",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية…",
|
||||
"filter": "إعادة حزم التصفية",
|
||||
"space_left_on_disk": "{{space}} متبقي على القرص",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"calculating_eta": "جارٍ حساب الوقت المتبقي...",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"filter": "تصفية الحزم المعاد تعبئتها",
|
||||
"requirements": "متطلبات النظام",
|
||||
"minimum": "الحد الأدنى",
|
||||
"recommended": "مُستَحسَن",
|
||||
"paused": "متوقف مؤقتًا",
|
||||
"release_date": "صدر بتاريخ {{date}}",
|
||||
"publisher": "نشرت من قبل {{publisher}}",
|
||||
"recommended": "مُوصى به",
|
||||
"paused": "معلّق",
|
||||
"release_date": "تاريخ الإصدار {{date}}",
|
||||
"publisher": "نشر بواسطة {{publisher}}",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"accuracy": "{{accuracy}}٪ دقة",
|
||||
"add_to_library": "أضف إلى المكتبة",
|
||||
"accuracy": "دقة {{accuracy}}%",
|
||||
"add_to_library": "إضافة إلى المكتبة",
|
||||
"remove_from_library": "إزالة من المكتبة",
|
||||
"no_downloads": "لا التنزيلات المتاحة",
|
||||
"play_time": "تم اللعب لمدة {{amount}}",
|
||||
"last_time_played": "لعبت آخر مرة {{period}}",
|
||||
"not_played_yet": "أنت لم تلعب {{title}} حتى الآن",
|
||||
"no_downloads": "لا توجد تنزيلات متاحة",
|
||||
"play_time": "لعب لمدة {{amount}}",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"not_played_yet": "لم تلعب {{title}} بعد",
|
||||
"next_suggestion": "الاقتراح التالي",
|
||||
"play": "لعب",
|
||||
"deleting": "جارٍ حذف المثبت…",
|
||||
"play": "تشغيل",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"close": "إغلاق",
|
||||
"playing_now": "قيداللعب الآن",
|
||||
"playing_now": "يتم التشغيل الآن",
|
||||
"change": "تغيير",
|
||||
"repacks_modal_description": "اختر الحزمة التي تريد تنزيلها",
|
||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>إعدادات</0>",
|
||||
"download_now": "قم بالتنزيل الآن",
|
||||
"no_shop_details": "لا يمكن استرداد تفاصيل المتجر.",
|
||||
"repacks_modal_description": "اختر الحزمة المعاد تعبئتها التي تريد تنزيلها",
|
||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
||||
"download_now": "تنزيل الآن",
|
||||
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
||||
"download_options": "خيارات التنزيل",
|
||||
"download_path": "مسار التحميل",
|
||||
"download_path": "مسار التنزيل",
|
||||
"previous_screenshot": "لقطة الشاشة السابقة",
|
||||
"next_screenshot": "لقطة الشاشة التالية",
|
||||
"screenshot": "لقطة الشاشة {{number}}",
|
||||
"open_screenshot": "فتح لقطة الشاشة {{number}}",
|
||||
"download_settings": "تحميل الإعدادات",
|
||||
"download_settings": "إعدادات التنزيل",
|
||||
"downloader": "أداة التنزيل",
|
||||
"select_executable": "يختار",
|
||||
"no_executable_selected": "لم يتم تحديد أي ملف قابل للتنفيذ",
|
||||
"open_folder": "افتح المجلد",
|
||||
"open_download_location": "انظر الملفات التي تم تنزيلها",
|
||||
"create_shortcut": "إنشاء اختصار سطح المكتب",
|
||||
"clear": "واضح",
|
||||
"select_executable": "تحديد",
|
||||
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
||||
"open_folder": "فتح المجلد",
|
||||
"open_download_location": "عرض الملفات المحملة",
|
||||
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
||||
"clear": "مسح",
|
||||
"remove_files": "إزالة الملفات",
|
||||
"remove_from_library_title": "هل أنت متأكد؟",
|
||||
"remove_from_library_description": "سيتم إزالة هذا {{game}} من مكتبتك",
|
||||
"remove_from_library_description": "سيؤدي هذا إلى إزالة {{game}} من مكتبتك",
|
||||
"options": "خيارات",
|
||||
"executable_section_title": "قابل للتنفيذ",
|
||||
"executable_section_description": "مسار الملف الذي سيتم تنفيذه عند النقر فوق \"تشغيل\".",
|
||||
"executable_section_title": "ملف التشغيل",
|
||||
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||
"downloads_secion_title": "التنزيلات",
|
||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى من هذه اللعبة",
|
||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||
"danger_zone_section_title": "منطقة الخطر",
|
||||
"danger_zone_section_description": "قم بإزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||
"download_in_progress": "التنزيل قيد التقدم",
|
||||
"download_paused": "تم إيقاف التنزيل مؤقتًا",
|
||||
"last_downloaded_option": "آخر خيار تم تنزيله",
|
||||
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||
"download_in_progress": "تنزيل قيد التقدم",
|
||||
"download_paused": "التنزيل معلق",
|
||||
"last_downloaded_option": "خيار التنزيل الأخير",
|
||||
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||
"create_shortcut_error": "حدث خطأ أثناء إنشاء الاختصار",
|
||||
"nsfw_content_title": "تحتوي هذه اللعبة على محتوى غير مناسب",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يكون مناسبًا لجميع الأعمار. ",
|
||||
"allow_nsfw_content": "اسمح",
|
||||
"refuse_nsfw_content": "عُد",
|
||||
"stats": "احصائيات",
|
||||
"download_count": "التنزيلات",
|
||||
"player_count": "اللاعبين النشطين",
|
||||
"download_error": "خيار التنزيل هذا غير متوفر",
|
||||
"download": "تحميل",
|
||||
"executable_path_in_use": "قابل للتنفيذ قيد الاستخدام بالفعل بواسطة \"{{game}}\"",
|
||||
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
||||
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يناسب جميع الأعمار. هل تريد المتابعة؟",
|
||||
"allow_nsfw_content": "متابعة",
|
||||
"refuse_nsfw_content": "رجوع",
|
||||
"stats": "الإحصائيات",
|
||||
"download_count": "مرات التنزيل",
|
||||
"player_count": "اللاعبون النشطون",
|
||||
"download_error": "خيار التنزيل هذا غير متاح",
|
||||
"download": "تنزيل",
|
||||
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
||||
"warning": "تحذير:",
|
||||
"hydra_needs_to_remain_open": "لإجراء هذا التنزيل، يجب أن يظل Hydra مفتوحًا حتى اكتماله. ",
|
||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يبقى Hydra مفتوحًا حتى اكتماله. إذا أغلق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||
"achievements": "الإنجازات",
|
||||
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "حفظ السحابة",
|
||||
"cloud_save_description": "احفظ تقدمك في السحابة واستمر في اللعب على أي جهاز",
|
||||
"cloud_save": "حفظ سحابي",
|
||||
"cloud_save_description": "احفظ تقدمك على السحابة واستمر في اللعب من أي جهاز",
|
||||
"backups": "النسخ الاحتياطية",
|
||||
"install_backup": "ثَبَّتَ",
|
||||
"delete_backup": "يمسح",
|
||||
"install_backup": "تثبيت",
|
||||
"delete_backup": "حذف",
|
||||
"create_backup": "نسخة احتياطية جديدة",
|
||||
"last_backup_date": "آخر نسخة احتياطية قيد التشغيل {{date}}",
|
||||
"no_backup_preview": "لم يتم العثور على ألعاب محفوظة لهذا العنوان",
|
||||
"restoring_backup": "استعادة النسخة الاحتياطية ({{progress}} مكتمل)…",
|
||||
"uploading_backup": "جارٍ تحميل النسخة الاحتياطية…",
|
||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة حتى الآن",
|
||||
"backup_uploaded": "تم تحميل النسخة الاحتياطية",
|
||||
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
||||
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
||||
"restoring_backup": "جارٍ استعادة النسخة الاحتياطية ({{progress}} اكتمال)...",
|
||||
"uploading_backup": "جارٍ رفع النسخة الاحتياطية...",
|
||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
||||
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
||||
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||
"backup_restored": "تمت استعادة النسخة الاحتياطية",
|
||||
"see_all_achievements": "شاهد جميع الإنجازات",
|
||||
"sign_in_to_see_achievements": "قم بتسجيل الدخول لرؤية الإنجازات",
|
||||
"backup_restored": "تم استعادة النسخة الاحتياطية",
|
||||
"see_all_achievements": "عرض جميع الإنجازات",
|
||||
"sign_in_to_see_achievements": "سجل الدخول لعرض الإنجازات",
|
||||
"mapping_method_automatic": "تلقائي",
|
||||
"mapping_method_manual": "يدوي",
|
||||
"mapping_method_label": "طريقة رسم الخرائط",
|
||||
"mapping_method_label": "طريقة التعيين",
|
||||
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||
"manage_files": "إدارة الملفات",
|
||||
"loading_save_preview": "جارٍ البحث عن حفظ الألعاب...",
|
||||
"wine_prefix": "بادئة النبيذ",
|
||||
"loading_save_preview": "جارٍ البحث عن حفظات الألعاب...",
|
||||
"wine_prefix": "بادئة Wine",
|
||||
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||
"launch_options": "خيارات التشغيل",
|
||||
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
||||
"launch_options_placeholder": "لم يتم تحديد أي معاملات",
|
||||
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||
"backup_deletion_failed": "فشل في حذف النسخة الاحتياطية",
|
||||
"backup_deletion_failed": "فشل حذف النسخة الاحتياطية",
|
||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
||||
"achievements_not_sync": "لا تتم مزامنة إنجازاتك",
|
||||
"achievements_not_sync": "تعرف على كيفية مزامنة إنجازاتك",
|
||||
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||
"select_folder": "حدد المجلد",
|
||||
"backup_from": "نسخة احتياطية من {{date}}",
|
||||
"custom_backup_location_set": "تعيين موقع النسخ الاحتياطي المخصص",
|
||||
"no_directory_selected": "لم يتم تحديد أي دليل",
|
||||
"download_options_one": "{{count}} خيار التنزيل",
|
||||
"download_options_two": "{{count}} خيارات التنزيل",
|
||||
"download_options_few": "{{count}} خيارات التنزيل",
|
||||
"download_options_many": "{{count}} خيارات التنزيل",
|
||||
"download_options_other": "{{count}} خيارات التنزيل"
|
||||
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
||||
"no_directory_selected": "لم يتم تحديد مجلد",
|
||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا لمعرفة المزيد.",
|
||||
"reset_achievements": "إعادة تعيين الإنجازات",
|
||||
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
||||
"reset_achievements_title": "هل أنت متأكد؟",
|
||||
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
||||
"reset_achievements_error": "فشل إعادة تعيين الإنجازات"
|
||||
},
|
||||
"activation": {
|
||||
"title": "تفعيل Hydra",
|
||||
"installation_id": "معرف التثبيت:",
|
||||
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا ينبغي أن يكون لديك هذا.",
|
||||
"activate": "فعل",
|
||||
"loading": "تحميل…"
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا يجب أن يكون لديك هذا.",
|
||||
"activate": "تفعيل",
|
||||
"loading": "جارٍ التحميل..."
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"eta": "الوقت المتبقي {{eta}}",
|
||||
"paused": "متوقف مؤقتًا",
|
||||
"verifying": "جارٍ التحقق…",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"paused": "معلّق",
|
||||
"verifying": "جارٍ التحقق...",
|
||||
"completed": "مكتمل",
|
||||
"removed": "لم يتم تحميلها",
|
||||
"removed": "غير محمل",
|
||||
"cancel": "إلغاء",
|
||||
"filter": "تصفية الألعاب التي تم تنزيلها",
|
||||
"filter": "تصفية الألعاب المحملة",
|
||||
"remove": "إزالة",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية…",
|
||||
"deleting": "جارٍ حذف المثبت…",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"delete": "إزالة المثبت",
|
||||
"delete_modal_title": "هل أنت متأكد؟",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة كافة ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
|
||||
"install": "ثَبَّتَ",
|
||||
"download_in_progress": "في تَقَدم",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
||||
"install": "تثبيت",
|
||||
"download_in_progress": "قيد التقدم",
|
||||
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||
"downloads_completed": "مكتمل",
|
||||
"queued": "في قائمة الانتظار",
|
||||
"no_downloads_title": "هذا فارغ",
|
||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان بعد للبدء.",
|
||||
"checking_files": "جارٍ فحص الملفات…"
|
||||
"no_downloads_title": "فارغ جدًا",
|
||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
||||
"checking_files": "جارٍ فحص الملفات...",
|
||||
"seeding": "التوزيع",
|
||||
"stop_seeding": "إيقاف التوزيع",
|
||||
"resume_seeding": "استئناف التوزيع",
|
||||
"options": "إدارة"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "مسار التنزيلات",
|
||||
"change": "تحديث",
|
||||
"notifications": "إشعارات",
|
||||
"notifications": "الإشعارات",
|
||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة جديدة",
|
||||
"real_debrid_api_token_label": "رمز Real-Debrid API",
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة معاد تعبئتها جديدة",
|
||||
"real_debrid_api_token_label": "رمز واجهة برمجة تطبيقات Real-Debrid",
|
||||
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
||||
"launch_with_system": "قم بتشغيل Hydra عند بدء تشغيل النظام",
|
||||
"launch_with_system": "تشغيل Hydra مع بدء النظام",
|
||||
"general": "عام",
|
||||
"behavior": "سلوك",
|
||||
"download_sources": "تحميل المصادر",
|
||||
"language": "لغة",
|
||||
"behavior": "السلوك",
|
||||
"download_sources": "مصادر التنزيل",
|
||||
"language": "اللغة",
|
||||
"real_debrid_api_token": "رمز API",
|
||||
"enable_real_debrid": "تمكين ريال ديبريد",
|
||||
"real_debrid_description": "Real-Debrid هو برنامج تنزيل غير مقيد يسمح لك بتنزيل الملفات بسرعة، ولا يقتصر ذلك إلا على سرعة الإنترنت لديك.",
|
||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
||||
"real_debrid_invalid_token": "رمز API غير صالح",
|
||||
"real_debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||
"real_debrid_linked_message": "حساب \"{{username}}\"مرتبط",
|
||||
"real_debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||
"save_changes": "حفظ التغييرات",
|
||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||
"download_sources_description": "ستقوم Hydra بجلب روابط التنزيل من هذه المصادر. ",
|
||||
"validate_download_source": "التحقق من صحة",
|
||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||
"validate_download_source": "تحقق",
|
||||
"remove_download_source": "إزالة",
|
||||
"add_download_source": "أضف المصدر",
|
||||
"download_count_zero": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_source_url": "تنزيل عنوان URL المصدر",
|
||||
"add_download_source": "إضافة مصدر",
|
||||
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
||||
"download_source_url": "عنوان URL لمصدر التنزيل",
|
||||
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||
"download_source_up_to_date": "محدث",
|
||||
"download_source_errored": "خطأ",
|
||||
"sync_download_sources": "مصادر المزامنة",
|
||||
"sync_download_sources": "مزامنة المصادر",
|
||||
"removed_download_source": "تمت إزالة مصدر التنزيل",
|
||||
"added_download_source": "تمت إضافة مصدر التنزيل",
|
||||
"download_sources_synced": "تتم مزامنة جميع مصادر التنزيل",
|
||||
"insert_valid_json_url": "أدخل عنوان URL صالحًا لـ JSON",
|
||||
"found_download_option_zero": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"import": "يستورد",
|
||||
"download_sources_synced": "تمت مزامنة جميع مصادر التنزيل",
|
||||
"insert_valid_json_url": "أدخل عنوان JSON صالح",
|
||||
"found_download_option_zero": "لم يتم العثور على خيارات تنزيل",
|
||||
"found_download_option_one": "تم العثور على {{countFormatted}} خيار تنزيل",
|
||||
"found_download_option_other": "تم العثور على {{countFormatted}} خيارات تنزيل",
|
||||
"import": "استيراد",
|
||||
"public": "عام",
|
||||
"private": "خاص",
|
||||
"friends_only": "الأصدقاء فقط",
|
||||
"privacy": "خصوصية",
|
||||
"privacy": "الخصوصية",
|
||||
"profile_visibility": "رؤية الملف الشخصي",
|
||||
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||
"required_field": "هذه الخانة مطلوبه",
|
||||
"source_already_exists": "تمت إضافة هذا المصدر بالفعل",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"source_already_exists": "تمت إضافة هذا المصدر مسبقًا",
|
||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
||||
"blocked_users": "المستخدمين المحظورين",
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||
"enable_achievement_notifications": "عندما يتم فتح الإنجاز",
|
||||
"launch_minimized": "تم تصغير إطلاق Hydra",
|
||||
"disable_nsfw_alert": "تعطيل تنبيه NSFW",
|
||||
"show_hidden_achievement_description": "إظهار وصف الإنجازات المخفية قبل فتحها",
|
||||
"download_count_one": "{{countFormatted}} خيار التنزيل",
|
||||
"download_count_two": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_count_few": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_count_many": "{{countFormatted}} خيارات التنزيل",
|
||||
"download_count_other": "{{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_one": "وجد {{countFormatted}} خيار التنزيل",
|
||||
"found_download_option_two": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_few": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_many": "وجد {{countFormatted}} خيارات التنزيل",
|
||||
"found_download_option_other": "وجد {{countFormatted}} خيارات التنزيل"
|
||||
"enable_achievement_notifications": "عند فتح إنجاز",
|
||||
"launch_minimized": "تشغيل Hydra مصغرًا",
|
||||
"disable_nsfw_alert": "تعطيل تنبيه المحتوى غير اللائق",
|
||||
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
||||
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
||||
"account": "الحساب",
|
||||
"no_users_blocked": "لا يوجد مستخدمون محظورون",
|
||||
"subscription_active_until": "اشتراك Hydra Cloud نشط حتى {{date}}",
|
||||
"manage_subscription": "إدارة الاشتراك",
|
||||
"update_email": "تحديث البريد الإلكتروني",
|
||||
"update_password": "تحديث كلمة المرور",
|
||||
"current_email": "البريد الإلكتروني الحالي:",
|
||||
"no_email_account": "لم تقم بتعيين بريد إلكتروني بعد",
|
||||
"account_data_updated_successfully": "تم تحديث بيانات الحساب بنجاح",
|
||||
"renew_subscription": "تجديد اشتراك Hydra Cloud",
|
||||
"subscription_expired_at": "انتهى اشتراكك في {{date}}",
|
||||
"no_subscription": "استمتع بـ Hydra بأفضل طريقة ممكنة",
|
||||
"become_subscriber": "كن مشتركًا في Hydra Cloud",
|
||||
"subscription_renew_cancelled": "تم تعطيل التجديد التلقائي",
|
||||
"subscription_renews_on": "سيتم تجديد اشتراكك في {{date}}",
|
||||
"bill_sent_until": "سيتم إرسال فاتورتك التالية حتى هذا اليوم"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "اكتمل التنزيل",
|
||||
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||
"repack_list_updated": "تم تحديث قائمة إعادة التعبئة",
|
||||
"new_update_available": "إصدار {{version}} متاح",
|
||||
"repack_list_updated": "تم تحديث قائمة الحزم المعاد تعبئتها",
|
||||
"repack_count_one": "تمت إضافة {{count}} حزمة معاد تعبئتها",
|
||||
"repack_count_other": "تمت إضافة {{count}} حزم معاد تعبئتها",
|
||||
"new_update_available": "الإصدار {{version}} متوفر",
|
||||
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
||||
"notification_achievement_unlocked_title": "تم فتح الإنجاز لـ {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} وغيرها {{count}} تم فتحها",
|
||||
"repack_count_zero": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_one": "{{count}} تمت إضافة أعد حزم",
|
||||
"repack_count_two": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_few": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_many": "{{count}} تمت إضافة العبوات",
|
||||
"repack_count_other": "{{count}} تمت إضافة العبوات"
|
||||
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} آخرين تم فتحهم"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "افتح Hydra",
|
||||
"open": "فتح Hydra",
|
||||
"quit": "خروج"
|
||||
},
|
||||
"game_card": {
|
||||
@@ -294,8 +318,8 @@
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "البرامج غير مثبتة",
|
||||
"description": "لم يتم العثور على الملفات التنفيذية الخاصة بـ Wine أو Lutris على نظامك",
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux لديك حتى تعمل اللعبة بشكل طبيعي"
|
||||
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة لينكس الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||
},
|
||||
"modal": {
|
||||
"close": "زر الإغلاق"
|
||||
@@ -306,33 +330,33 @@
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"last_time_played": "لعبت آخر مرة {{period}}",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"activity": "النشاط الأخير",
|
||||
"library": "مكتبة",
|
||||
"library": "المكتبة",
|
||||
"total_play_time": "إجمالي وقت اللعب",
|
||||
"no_recent_activity_title": "هممم... لا شيء هنا",
|
||||
"no_recent_activity_description": "لم تلعب أي مباراة مؤخرًا. ",
|
||||
"no_recent_activity_title": "همم... لا شيء هنا",
|
||||
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
||||
"display_name": "اسم العرض",
|
||||
"saving": "توفير",
|
||||
"save": "يحفظ",
|
||||
"edit_profile": "تحرير الملف الشخصي",
|
||||
"saving": "جارٍ الحفظ",
|
||||
"save": "حفظ",
|
||||
"edit_profile": "تعديل الملف الشخصي",
|
||||
"saved_successfully": "تم الحفظ بنجاح",
|
||||
"try_again": "من فضلك، حاول مرة أخرى",
|
||||
"try_again": "يرجى المحاولة مرة أخرى",
|
||||
"sign_out_modal_title": "هل أنت متأكد؟",
|
||||
"cancel": "إلغاء",
|
||||
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||
"sign_out": "تسجيل الخروج",
|
||||
"playing_for": "اللعب من أجل {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. ",
|
||||
"add_friends": "أضف أصدقاء",
|
||||
"add": "يضيف",
|
||||
"playing_for": "يلعب لمدة {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية بعد الآن، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||
"add_friends": "إضافة أصدقاء",
|
||||
"add": "إضافة",
|
||||
"friend_code": "رمز الصديق",
|
||||
"see_profile": "انظر الملف الشخصي",
|
||||
"sending": "إرسال",
|
||||
"see_profile": "عرض الملف الشخصي",
|
||||
"sending": "جارٍ الإرسال",
|
||||
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||
"friends": "أصدقاء",
|
||||
"friends": "الأصدقاء",
|
||||
"friends_list": "قائمة الأصدقاء",
|
||||
"user_not_found": "لم يتم العثور على المستخدم",
|
||||
"user_not_found": "المستخدم غير موجود",
|
||||
"block_user": "حظر المستخدم",
|
||||
"add_friend": "إضافة صديق",
|
||||
"request_sent": "تم إرسال الطلب",
|
||||
@@ -340,63 +364,76 @@
|
||||
"accept_request": "قبول الطلب",
|
||||
"ignore_request": "تجاهل الطلب",
|
||||
"cancel_request": "إلغاء الطلب",
|
||||
"undo_friendship": "التراجع عن الصداقة",
|
||||
"undo_friendship": "إلغاء الصداقة",
|
||||
"request_accepted": "تم قبول الطلب",
|
||||
"user_blocked_successfully": "تم حظر المستخدم بنجاح",
|
||||
"user_block_modal_text": "هذا سوف يمنع {{displayName}}",
|
||||
"blocked_users": "المستخدمين المحظورين",
|
||||
"user_block_modal_text": "سيؤدي هذا إلى حظر {{displayName}}",
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"unblock": "إلغاء الحظر",
|
||||
"no_friends_added": "ليس لديك أي أصدقاء مضافين",
|
||||
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
||||
"pending": "قيد الانتظار",
|
||||
"no_pending_invites": "ليس لديك أي دعوات معلقة",
|
||||
"no_blocked_users": "ليس لديك أي مستخدمين محظورين",
|
||||
"no_pending_invites": "ليس لديك دعوات معلقة",
|
||||
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
||||
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||
"undo_friendship_modal_text": "سيؤدي هذا إلى التراجع عن صداقتك معه {{displayName}}",
|
||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>إعدادات</0>",
|
||||
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
||||
"locked_profile": "هذا الملف الشخصي خاص",
|
||||
"image_process_failure": "فشل أثناء معالجة الصورة",
|
||||
"required_field": "هذه الخانة مطلوبه",
|
||||
"displayname_min_length": "يجب أن يتكون اسم العرض من 3 أحرف على الأقل",
|
||||
"displayname_max_length": "يجب ألا يزيد طول اسم العرض عن 50 حرفًا",
|
||||
"image_process_failure": "فشل معالجة الصورة",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
||||
"displayname_max_length": "يجب ألا يتجاوز اسم العرض 50 حرفًا",
|
||||
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||
"report_description": "معلومات إضافية",
|
||||
"report_description_placeholder": "معلومات إضافية",
|
||||
"report": "تقرير",
|
||||
"report_reason_hate": "خطاب الكراهية",
|
||||
"report_reason_sexual_content": "المحتوى الجنسي",
|
||||
"report": "الإبلاغ",
|
||||
"report_reason_hate": "خطاب كراهية",
|
||||
"report_reason_sexual_content": "محتوى جنسي",
|
||||
"report_reason_violence": "عنف",
|
||||
"report_reason_spam": "رسائل إلكترونية مزعجة",
|
||||
"report_reason_spam": "بريد عشوائي",
|
||||
"report_reason_other": "أخرى",
|
||||
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||
"your_friend_code": "رمز صديقك:",
|
||||
"upload_banner": "تحميل لافتة",
|
||||
"uploading_banner": "جارٍ تحميل البانر…",
|
||||
"upload_banner": "تحميل بانر",
|
||||
"uploading_banner": "جارٍ تحميل البانر...",
|
||||
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||
"report_reason_zero": "آخر",
|
||||
"report_reason_one": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||
"report_reason_two": "آخر",
|
||||
"report_reason_few": "آخر",
|
||||
"report_reason_many": "آخر",
|
||||
"report_reason_other": "آخر"
|
||||
"stats": "الإحصائيات",
|
||||
"achievements": "إنجازات",
|
||||
"games": "الألعاب",
|
||||
"top_percentile": "ال{{percentile}}% الأعلى",
|
||||
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
||||
"playing": "يلعب {{game}}",
|
||||
"achievements_unlocked": "الإنجازات المفتوحة",
|
||||
"earned_points": "النقاط المكتسبة",
|
||||
"show_achievements_on_profile": "عرض إنجازاتك على ملفك الشخصي",
|
||||
"show_points_on_profile": "عرض نقاطك المكتسبة على ملفك الشخصي"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "تم فتح الإنجاز",
|
||||
"user_achievements": "{{displayName}}إنجازات",
|
||||
"user_achievements": "إنجازات {{displayName}}",
|
||||
"your_achievements": "إنجازاتك",
|
||||
"unlocked_at": "مقفلة في: {{date}}",
|
||||
"subscription_needed": "مطلوب اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
||||
"new_achievements_unlocked": "مفتوح {{achievementCount}} انجازات جديدة من {{gameCount}} ألعاب",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} الإنجازات",
|
||||
"achievements_unlocked_for_game": "مفتوح {{achievementCount}} انجازات جديدة ل {{gameTitle}}"
|
||||
"unlocked_at": "تم الفتح في: {{date}}",
|
||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
||||
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
||||
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
||||
"achievement_earn_points": "اكسب {{points}} نقطة مع هذا الإنجاز",
|
||||
"earned_points": "النقاط المكتسبة:",
|
||||
"available_points": "النقاط المتاحة:",
|
||||
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "اشتراك Hydra كلاود",
|
||||
"subscription_tour_title": "اشتراك Hydra Cloud",
|
||||
"subscribe_now": "اشترك الآن",
|
||||
"cloud_saving": "الحفظ السحابي",
|
||||
"cloud_saving": "حفظ سحابي",
|
||||
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||
"animated_profile_picture": "صور شخصية متحركة",
|
||||
"premium_support": "دعم متميز",
|
||||
"show_and_compare_achievements": "عرض ومقارنة إنجازاتك مع المستخدمين الآخرين",
|
||||
"animated_profile_banner": "لافتة الملف الشخصي المتحركة"
|
||||
"animated_profile_picture": "صورة ملف شخصي متحركة",
|
||||
"premium_support": "دعم ممتاز",
|
||||
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
||||
"animated_profile_banner": "بانر ملف شخصي متحرك",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
||||
"learn_more": "معرفة المزيد"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"game_details": {
|
||||
"launch_options": "Опции за стартиране",
|
||||
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране",
|
||||
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране (экспериментальный)",
|
||||
"launch_options_placeholder": "Няма зададен параметър",
|
||||
"open_download_options": "Варианти за изтегляне",
|
||||
"download_options_zero": "Няма варианти за изтегляне",
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "The Wine prefix used to run this game",
|
||||
"launch_options": "Launch Options",
|
||||
"launch_options_description": "Advanced users may choose to enter modifications to their launch options",
|
||||
"launch_options_description": "Advanced users may choose to enter modifications to their launch options (experimental feature)",
|
||||
"launch_options_placeholder": "No parameter specified",
|
||||
"no_download_option_info": "No information available",
|
||||
"backup_deletion_failed": "Failed to delete backup",
|
||||
@@ -178,7 +178,13 @@
|
||||
"select_folder": "Select folder",
|
||||
"backup_from": "Backup from {{date}}",
|
||||
"custom_backup_location_set": "Custom backup location set",
|
||||
"no_directory_selected": "No directory selected"
|
||||
"no_directory_selected": "No directory selected",
|
||||
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
||||
"reset_achievements": "Reset achievements",
|
||||
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
||||
"reset_achievements_title": "Are you sure?",
|
||||
"reset_achievements_success": "Achievements successfully reset",
|
||||
"reset_achievements_error": "Failed to reset achievements"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@@ -274,7 +280,23 @@
|
||||
"launch_minimized": "Launch Hydra minimized",
|
||||
"disable_nsfw_alert": "Disable NSFW alert",
|
||||
"seed_after_download_complete": "Seed after download complete",
|
||||
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them"
|
||||
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
|
||||
"account": "Account",
|
||||
"no_users_blocked": "You have no blocked users",
|
||||
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
|
||||
"manage_subscription": "Manage subscription",
|
||||
"update_email": "Update email",
|
||||
"update_password": "Update password",
|
||||
"current_email": "Current email:",
|
||||
"no_email_account": "You have not set an email yet",
|
||||
"account_data_updated_successfully": "Account data updated successfully",
|
||||
"renew_subscription": "Renew Hydra Cloud",
|
||||
"subscription_expired_at": "Your subscription expired at {{date}}",
|
||||
"no_subscription": "Enjoy Hydra in the best possible way",
|
||||
"become_subscriber": "Be Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||
"bill_sent_until": "Your next bill will be sent until this day"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"wine_prefix": "Prefixo Wine",
|
||||
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
|
||||
"launch_options": "Opções de Inicialização",
|
||||
"launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo",
|
||||
"launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo (experimental)",
|
||||
"launch_options_placeholder": "Nenhum parâmetro informado",
|
||||
"no_download_option_info": "Sem informações disponíveis",
|
||||
"backup_deletion_failed": "Falha ao apagar backup",
|
||||
@@ -167,7 +167,12 @@
|
||||
"select_folder": "Selecione a pasta",
|
||||
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
||||
"clear": "Limpar",
|
||||
"no_directory_selected": "Nenhum diretório selecionado"
|
||||
"no_directory_selected": "Nenhum diretório selecionado",
|
||||
"reset_achievements": "Resetar conquistas",
|
||||
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
||||
"reset_achievements_title": "Tem certeza?",
|
||||
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
||||
"reset_achievements_error": "Falha ao resetar conquistas"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -263,7 +268,23 @@
|
||||
"launch_minimized": "Iniciar o Hydra minimizado",
|
||||
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
|
||||
"seed_after_download_complete": "Semear após a conclusão do download",
|
||||
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las"
|
||||
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
|
||||
"account": "Conta",
|
||||
"no_users_blocked": "Você não bloqueou nenhum usuário",
|
||||
"subscription_active_until": "Sua assinatura Hydra Cloud ficará ativa até {{date}}",
|
||||
"manage_subscription": "Gerenciar assinatura",
|
||||
"update_email": "Atualizar email",
|
||||
"update_password": "Atualizar senha",
|
||||
"current_email": "Email atual:",
|
||||
"no_email_account": "Você ainda não adicionou um email a sua conta",
|
||||
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_expired_at": "Sua assinatura expirou em {{date}}",
|
||||
"no_subscription": "Aproveite o Hydra da melhor forma possível",
|
||||
"become_subscriber": "Seja Hydra Cloud",
|
||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
@@ -392,7 +413,7 @@
|
||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
||||
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Está é uma conquista oculta",
|
||||
"hidden_achievement_tooltip": "Esta é uma conquista oculta",
|
||||
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
|
||||
"earned_points": "Pontos ganhos:",
|
||||
"available_points": "Pontos disponíveis:",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"featured": "Рекомендации",
|
||||
"surprise_me": "Удиви меня",
|
||||
"no_results": "Ничего не найдено",
|
||||
"hot": "Сейчас в топе",
|
||||
"start_typing": "Начинаю вводить текст для поиска...",
|
||||
"hot": "Сейчас популярно",
|
||||
"start_typing": "Начинаю вводить текст...",
|
||||
"weekly": "📅 Лучшие игры недели",
|
||||
"achievements": "🏆 Игры, в которых нужно победить"
|
||||
},
|
||||
@@ -167,6 +167,9 @@
|
||||
"loading_save_preview": "Поиск сохранений…",
|
||||
"wine_prefix": "Префикс Wine",
|
||||
"wine_prefix_description": "Префикс Wine, используемый для запуска этой игры",
|
||||
"launch_options": "Параметры запуска",
|
||||
"launch_options_description": "Опытные пользователи могут внести изменения в параметры запуска",
|
||||
"launch_options_placeholder": "Параметр не указан ",
|
||||
"no_download_option_info": "Информация недоступна",
|
||||
"backup_deletion_failed": "Не удалось удалить резервную копию",
|
||||
"max_number_of_artifacts_reached": "Достигнуто максимальное количество резервных копий для этой игры",
|
||||
@@ -175,7 +178,11 @@
|
||||
"select_folder": "Выбрать папку",
|
||||
"backup_from": "Резервная копия от {{date}}",
|
||||
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
||||
"no_directory_selected": "Не выбран каталог"
|
||||
"no_directory_selected": "Не выбран каталог",
|
||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||
"reset_achievements_title": "Вы уверены?",
|
||||
"reset_achievements_success": "Достижения успешно сброшены",
|
||||
"reset_achievements_error": "Не удалось сбросить достижения"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активировать Hydra",
|
||||
@@ -271,7 +278,23 @@
|
||||
"source_already_exists": "Этот источник уже добавлен",
|
||||
"user_unblocked": "Пользователь разблокирован",
|
||||
"seed_after_download_complete": "Раздавать после завершения загрузки",
|
||||
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением"
|
||||
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
|
||||
"account": "Аккаунт",
|
||||
"no_users_blocked": "У вас нет заблокированных пользователей",
|
||||
"subscription_active_until": "Ваша подписка на Hydra Cloud активна до {{date}}",
|
||||
"manage_subscription": "Управлять подпиской",
|
||||
"update_email": "Обновить электронную почту",
|
||||
"update_password": "Обновить пароль",
|
||||
"current_email": "Текущий email:",
|
||||
"no_email_account": "Вы еще не установили электронную почту",
|
||||
"account_data_updated_successfully": "Данные учетной записи успешно обновлены",
|
||||
"renew_subscription": "Обновить подписку Hydra Cloud",
|
||||
"subscription_expired_at": "Срок действия вашей подписки истек в {{date}}",
|
||||
"no_subscription": "Наслаждайтесь Hydra по максимуму",
|
||||
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
||||
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
||||
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
@@ -401,7 +424,7 @@
|
||||
"subscribe_now": "Подпишитесь прямо сейчас",
|
||||
"cloud_saving": "Сохранение в облаке",
|
||||
"cloud_achievements": "Сохраняйте свои достижения в облаке",
|
||||
"animated_profile_picture": "Анимированные фотографии профиля",
|
||||
"animated_profile_picture": "Анимированные аватарки",
|
||||
"premium_support": "Премиальная поддержка",
|
||||
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
|
||||
"animated_profile_banner": "Анимированный баннер профиля",
|
||||
|
||||
@@ -1,131 +1,423 @@
|
||||
{
|
||||
"language_name": "Türkçe",
|
||||
"app": {
|
||||
"successfully_signed_in": "Başarıyla giriş yapıldı"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Öne çıkan",
|
||||
"surprise_me": "Şaşırt beni",
|
||||
"no_results": "Sonuç bulunamadı"
|
||||
"featured": "Öne Çıkanlar",
|
||||
"surprise_me": "Beni Şaşırt",
|
||||
"no_results": "Sonuç bulunamadı",
|
||||
"start_typing": "Aramak için yazmaya başlayın...",
|
||||
"hot": "Şu anda popüler",
|
||||
"weekly": "📅 Haftanın en iyi oyunları",
|
||||
"achievements": "🏆 Tamamlanacak oyunlar"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirmeler",
|
||||
"downloads": "İndirilenler",
|
||||
"settings": "Ayarlar",
|
||||
"my_library": "Kütüphane",
|
||||
"downloading_metadata": "{{title}} (Metadata indiriliyor…)",
|
||||
"paused": "{{title}} (Duraklatıldı)",
|
||||
"my_library": "Kütüphanem",
|
||||
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
|
||||
"paused": "{{title}} (Durduruldu)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphaneyi filtrele",
|
||||
"home": "Ana menü"
|
||||
"home": "Ana Sayfa",
|
||||
"queued": "{{title}} (Sırada)",
|
||||
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
||||
"sign_in": "Giriş yap",
|
||||
"friends": "Arkadaşlar",
|
||||
"need_help": "Yardıma mı ihtiyacınız var?"
|
||||
},
|
||||
"header": {
|
||||
"search": "Ara",
|
||||
"home": "Ana menü",
|
||||
"search": "Oyunları ara",
|
||||
"home": "Ana Sayfa",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirmeler",
|
||||
"downloads": "İndirilenler",
|
||||
"search_results": "Arama sonuçları",
|
||||
"settings": "Ayarlar"
|
||||
"settings": "Ayarlar",
|
||||
"version_available_install": "Sürüm {{version}} mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "Sürüm {{version}} mevcut. İndirmek için buraya tıklayın."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "İndirilen bir şey yok",
|
||||
"downloading_metadata": "{{title}} metadatası indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
|
||||
"no_downloads_in_progress": "Devam eden indirme yok",
|
||||
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlama: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Sonraki sayfa",
|
||||
"previous_page": "Önceki sayfa"
|
||||
"search": "Filtrele…",
|
||||
"developers": "Geliştiriciler",
|
||||
"genres": "Türler",
|
||||
"tags": "Etiketler",
|
||||
"publishers": "Yayıncılar",
|
||||
"download_sources": "İndirme kaynakları",
|
||||
"result_count": "{{resultCount}} sonuç",
|
||||
"filter_count": "{{filterCount}} mevcut",
|
||||
"clear_filters": "{{filterCount}} seçili filtreyi temizle"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "İndirme seçeneklerini aç",
|
||||
"download_options_zero": "İndirme seçeneği yok",
|
||||
"download_options_one": "{{count}} indirme seçeneği",
|
||||
"download_options_other": "{{count}} indirme seçeneği",
|
||||
"updated_at": "{{updated_at}} güncellendi",
|
||||
"install": "İndir",
|
||||
"updated_at": "{{updated_at}} tarihinde güncellendi",
|
||||
"install": "Yükle",
|
||||
"resume": "Devam et",
|
||||
"pause": "Duraklat",
|
||||
"pause": "Durdur",
|
||||
"cancel": "İptal et",
|
||||
"remove": "Sil",
|
||||
"space_left_on_disk": "Diskte {{space}} yer kaldı",
|
||||
"eta": "Bitiş {{eta}}",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"filter": "Repackleri filtrele",
|
||||
"remove": "Kaldır",
|
||||
"space_left_on_disk": "Diskte {{space}} boş alan kaldı",
|
||||
"eta": "{{eta}} tahmini bitiş",
|
||||
"calculating_eta": "Kalan süre hesaplanıyor…",
|
||||
"downloading_metadata": "Meta veriler indiriliyor…",
|
||||
"filter": "Paketleri filtrele",
|
||||
"requirements": "Sistem gereksinimleri",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Önerilen",
|
||||
"release_date": "{{date}} tarihinde çıktı",
|
||||
"publisher": "{{publisher}} tarihinde yayınlandı",
|
||||
"hours": "saatler",
|
||||
"minutes": "dakikalar",
|
||||
"paused": "Durduruldu",
|
||||
"release_date": "{{date}} tarihinde yayımlandı",
|
||||
"publisher": "{{publisher}} tarafından yayımlandı",
|
||||
"hours": "saat",
|
||||
"minutes": "dakika",
|
||||
"amount_hours": "{{amount}} saat",
|
||||
"amount_minutes": "{{amount}} dakika",
|
||||
"accuracy": "%{{accuracy}} doğruluk",
|
||||
"accuracy": "{{accuracy}}% doğruluk",
|
||||
"add_to_library": "Kütüphaneye ekle",
|
||||
"remove_from_library": "Kütüphaneden kaldır",
|
||||
"no_downloads": "İndirme yok",
|
||||
"play_time": "{{amount}} oynandı",
|
||||
"last_time_played": "Son oynanan {{period}}",
|
||||
"not_played_yet": "Bu {{title}} hiç oynanmadı",
|
||||
"next_suggestion": "Sıradaki öneri",
|
||||
"no_downloads": "İndirilebilir içerik yok",
|
||||
"play_time": "{{amount}} süre oynandı",
|
||||
"last_time_played": "Son oynama {{period}} önce",
|
||||
"not_played_yet": "{{title}} henüz oynanmadı",
|
||||
"next_suggestion": "Sonraki öneri",
|
||||
"play": "Oyna",
|
||||
"deleting": "Installer siliniyor…",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"close": "Kapat",
|
||||
"playing_now": "Şimdi oynanıyor",
|
||||
"playing_now": "Şu anda oynanıyor",
|
||||
"change": "Değiştir",
|
||||
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
|
||||
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
|
||||
"download_now": "Şimdi"
|
||||
"repacks_modal_description": "İndirmek istediğiniz paketi seçin",
|
||||
"select_folder_hint": "Varsayılan klasörü değiştirmek için <0>Ayarlar</0> bölümüne gidin",
|
||||
"download_now": "Şimdi indir",
|
||||
"no_shop_details": "Mağaza bilgileri alınamadı.",
|
||||
"download_options": "İndirme seçenekleri",
|
||||
"download_path": "İndirme yolu",
|
||||
"previous_screenshot": "Önceki ekran görüntüsü",
|
||||
"next_screenshot": "Sonraki ekran görüntüsü",
|
||||
"screenshot": "{{number}} ekran görüntüsü",
|
||||
"open_screenshot": "{{number}} ekran görüntüsünü aç",
|
||||
"download_settings": "İndirme ayarları",
|
||||
"downloader": "İndirici",
|
||||
"select_executable": "Seç",
|
||||
"no_executable_selected": "Hiçbir çalıştırılabilir dosya seçilmedi",
|
||||
"open_folder": "Klasörü aç",
|
||||
"open_download_location": "İndirilen dosyaları gör",
|
||||
"create_shortcut": "Masaüstü kısayolu oluştur",
|
||||
"clear": "Temizle",
|
||||
"remove_files": "Dosyaları kaldır",
|
||||
"remove_from_library_title": "Emin misiniz?",
|
||||
"remove_from_library_description": "Bu işlem {{game}} oyununu kütüphanenizden kaldıracaktır",
|
||||
"options": "Seçenekler",
|
||||
"executable_section_title": "Çalıştırılabilir dosya",
|
||||
"executable_section_description": "\"Oyna\" tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"downloads_secion_title": "İndirmeler",
|
||||
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
|
||||
"danger_zone_section_title": "Tehlike bölgesi",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyaları kaldırın",
|
||||
"download_in_progress": "İndirme devam ediyor",
|
||||
"download_paused": "İndirme durduruldu",
|
||||
"last_downloaded_option": "Son indirilen seçenek",
|
||||
"create_shortcut_success": "Kısayol başarıyla oluşturuldu",
|
||||
"create_shortcut_error": "Kısayol oluşturulurken hata oluştu",
|
||||
"nsfw_content_title": "Bu oyun uygunsuz içerik içeriyor",
|
||||
"nsfw_content_description": "{{title}} her yaş için uygun olmayabilecek içeriklere sahiptir. Devam etmek istediğinizden emin misiniz?",
|
||||
"allow_nsfw_content": "Devam et",
|
||||
"refuse_nsfw_content": "Geri dön",
|
||||
"stats": "İstatistikler",
|
||||
"download_count": "İndirme sayısı",
|
||||
"player_count": "Aktif oyuncular",
|
||||
"download_error": "Bu indirme seçeneği mevcut değil",
|
||||
"download": "İndir",
|
||||
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
|
||||
"warning": "Uyarı:",
|
||||
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
|
||||
"achievements": "Başarılar",
|
||||
"achievements_count": "Başarılar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Bulut kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
|
||||
"backups": "Yedekler",
|
||||
"install_backup": "Yükle",
|
||||
"delete_backup": "Sil",
|
||||
"create_backup": "Yeni yedek oluştur",
|
||||
"last_backup_date": "{{date}} tarihindeki son yedek",
|
||||
"no_backup_preview": "Bu oyun için kayıtlı oyun bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
|
||||
"uploading_backup": "Yedek yükleniyor…",
|
||||
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
|
||||
"backup_uploaded": "Yedek yüklendi",
|
||||
"backup_deleted": "Yedek silindi",
|
||||
"backup_restored": "Yedek geri yüklendi",
|
||||
"see_all_achievements": "Tüm başarıları gör",
|
||||
"sign_in_to_see_achievements": "Başarıları görmek için giriş yapın",
|
||||
"mapping_method_automatic": "Otomatik",
|
||||
"mapping_method_manual": "Manuel",
|
||||
"mapping_method_label": "Eşleme yöntemi",
|
||||
"files_automatically_mapped": "Dosyalar otomatik olarak eşlendi",
|
||||
"no_backups_created": "Bu oyun için yedek oluşturulmadı",
|
||||
"manage_files": "Dosyaları yönet",
|
||||
"loading_save_preview": "Kayıtlı oyunlar aranıyor…",
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
|
||||
"launch_options": "Başlatma Seçenekleri",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine değişiklikler girebilir (deneysel özellik)",
|
||||
"launch_options_placeholder": "Belirtilen bir parametre yok",
|
||||
"no_download_option_info": "Bilgi mevcut değil",
|
||||
"backup_deletion_failed": "Yedek silinemedi",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarılarınızı senkronize etmeyi öğrenin",
|
||||
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
|
||||
"select_folder": "Klasör seç",
|
||||
"backup_from": "{{date}} tarihinden yedek",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
|
||||
"no_directory_selected": "Bir dizin seçilmedi",
|
||||
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
|
||||
"reset_achievements": "Başarıları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarıları sıfırlar",
|
||||
"reset_achievements_title": "Emin misiniz?",
|
||||
"reset_achievements_success": "Başarılar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarılar sıfırlanamadı"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'yı aktif et",
|
||||
"installation_id": "Kurulum ID'si:",
|
||||
"enter_activation_code": "Aktifleştirme kodunuzu girin",
|
||||
"message": "Bunu nerede soracağınızı bilmiyorsanız, buna sahip olmamanız gerekiyor.",
|
||||
"activate": "Aktif et",
|
||||
"title": "Hydra'yı Aktive Et",
|
||||
"installation_id": "Kurulum Kimliği:",
|
||||
"enter_activation_code": "Aktivasyon kodunuzu girin",
|
||||
"message": "Bunu nereden soracağınızı bilmiyorsanız, bu sizin için olmamalı.",
|
||||
"activate": "Aktive Et",
|
||||
"loading": "Yükleniyor…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Devam et",
|
||||
"resume": "Devam Et",
|
||||
"pause": "Duraklat",
|
||||
"eta": "Bitiş {{eta}}",
|
||||
"eta": "Tamamlama {{eta}}",
|
||||
"paused": "Duraklatıldı",
|
||||
"verifying": "Doğrulanıyor…",
|
||||
"completed": "Tamamlandı",
|
||||
"cancel": "İptal et",
|
||||
"filter": "Yüklü oyunları filtrele",
|
||||
"removed": "İndirilmedi",
|
||||
"cancel": "İptal Et",
|
||||
"filter": "İndirilen oyunları filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"deleting": "Installer siliniyor…",
|
||||
"delete": "Installer'ı sil",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"delete": "Yükleyiciyi kaldır",
|
||||
"delete_modal_title": "Emin misiniz?",
|
||||
"delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek",
|
||||
"install": "Kur"
|
||||
"delete_modal_description": "Bu işlem, tüm kurulum dosyalarını bilgisayarınızdan kaldıracaktır",
|
||||
"install": "Kur",
|
||||
"download_in_progress": "Devam ediyor",
|
||||
"queued_downloads": "Sıradaki indirmeler",
|
||||
"downloads_completed": "Tamamlananlar",
|
||||
"queued": "Sırada",
|
||||
"no_downloads_title": "Bomboş",
|
||||
"no_downloads_description": "Henüz Hydra ile hiçbir şey indirmediniz, ancak başlamak için asla geç değil.",
|
||||
"checking_files": "Dosyalar kontrol ediliyor…",
|
||||
"seeding": "Paylaşılıyor",
|
||||
"stop_seeding": "Paylaşımı durdur",
|
||||
"resume_seeding": "Paylaşımı sürdür",
|
||||
"options": "Yönet"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "İndirme yolu",
|
||||
"change": "Güncelle",
|
||||
"notifications": "Bildirimler",
|
||||
"enable_download_notifications": "Bir indirme bittiğinde",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde"
|
||||
"enable_download_notifications": "Bir indirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
|
||||
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatırken gizlemeyin",
|
||||
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
|
||||
"general": "Genel",
|
||||
"behavior": "Davranış",
|
||||
"download_sources": "İndirme kaynakları",
|
||||
"language": "Dil",
|
||||
"real_debrid_api_token": "API Anahtarı",
|
||||
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
|
||||
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
|
||||
"real_debrid_invalid_token": "Geçersiz API anahtarı",
|
||||
"real_debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
|
||||
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
|
||||
"real_debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
||||
"save_changes": "Değişiklikleri Kaydet",
|
||||
"changes_saved": "Değişiklikler başarıyla kaydedildi",
|
||||
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
|
||||
"validate_download_source": "Doğrula",
|
||||
"remove_download_source": "Kaldır",
|
||||
"add_download_source": "Kaynak ekle",
|
||||
"download_count_zero": "İndirme seçeneği yok",
|
||||
"download_count_one": "{{countFormatted}} indirme seçeneği",
|
||||
"download_count_other": "{{countFormatted}} indirme seçeneği",
|
||||
"download_source_url": "İndirme kaynağı URL'si",
|
||||
"add_download_source_description": ".json dosyasının URL'sini girin",
|
||||
"download_source_up_to_date": "Güncel",
|
||||
"download_source_errored": "Hatalı",
|
||||
"sync_download_sources": "Kaynakları senkronize et",
|
||||
"removed_download_source": "İndirme kaynağı kaldırıldı",
|
||||
"added_download_source": "İndirme kaynağı eklendi",
|
||||
"download_sources_synced": "Tüm indirme kaynakları senkronize edildi",
|
||||
"insert_valid_json_url": "Geçerli bir JSON URL'si girin",
|
||||
"found_download_option_zero": "Hiçbir indirme seçeneği bulunamadı",
|
||||
"found_download_option_one": "{{countFormatted}} indirme seçeneği bulundu",
|
||||
"found_download_option_other": "{{countFormatted}} indirme seçeneği bulundu",
|
||||
"import": "İçe aktar",
|
||||
"public": "Herkese açık",
|
||||
"private": "Gizli",
|
||||
"friends_only": "Sadece arkadaşlar",
|
||||
"privacy": "Gizlilik",
|
||||
"profile_visibility": "Profil görünürlüğü",
|
||||
"profile_visibility_description": "Profilinizi ve kütüphanenizi kimlerin görebileceğini seçin",
|
||||
"required_field": "Bu alan gereklidir",
|
||||
"source_already_exists": "Bu kaynak zaten eklenmiş",
|
||||
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"user_unblocked": "Kullanıcının engeli kaldırıldı",
|
||||
"enable_achievement_notifications": "Bir başarı kilidi açıldığında",
|
||||
"launch_minimized": "Hydra'yı küçültülmüş başlat",
|
||||
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
|
||||
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
|
||||
"show_hidden_achievement_description": "Gizli başarı açıklamalarını kilitlenmeden önce göster"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "İndirme tamamlandı",
|
||||
"game_ready_to_install": "{{title}} kuruluma hazır",
|
||||
"game_ready_to_install": "{{title}} kurulmaya hazır",
|
||||
"repack_list_updated": "Repack listesi güncellendi",
|
||||
"repack_count_one": "{{count}} yeni repack eklendi",
|
||||
"repack_count_other": "{{count}} yeni repack eklendi"
|
||||
"repack_count_one": "{{count}} repack eklendi",
|
||||
"repack_count_other": "{{count}} repack eklendi",
|
||||
"new_update_available": "Sürüm {{version}} mevcut",
|
||||
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarı kilidi açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarılar açıldı"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra'yı aç",
|
||||
"open": "Hydra'yı Aç",
|
||||
"quit": "Çık"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "İndirme mevcut değil"
|
||||
"no_downloads": "İndirilebilir içerik bulunmuyor"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programlar yüklü değil",
|
||||
"description": "Sisteminizde Wine veya Lutris çalıştırılabiliri bulunamadı",
|
||||
"instructions": "Oyunları düzgün şekilde çalıştırmak için Linux distronuza bunlardan birini nasıl yükleyebileceğinize bakın"
|
||||
"title": "Programlar Yüklü Değil",
|
||||
"description": "Wine veya Lutris çalıştırılabilir dosyaları sisteminizde bulunamadı",
|
||||
"instructions": "Oyunun normal çalışabilmesi için bunlardan herhangi birini Linux dağıtımınıza uygun şekilde nasıl kuracağınızı kontrol edin"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Kapat tuşu"
|
||||
"close": "Kapat düğmesi"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Şifre görünürlüğünü değiştir"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} saat",
|
||||
"amount_minutes": "{{amount}} dakika",
|
||||
"last_time_played": "Son oynanma {{period}}",
|
||||
"activity": "Son Etkinlik",
|
||||
"library": "Kütüphane",
|
||||
"total_play_time": "Toplam oynama süresi",
|
||||
"no_recent_activity_title": "Hmmm… burada bir şey yok",
|
||||
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamamışsınız. Bunu değiştirmenin zamanı geldi!",
|
||||
"display_name": "Görünen isim",
|
||||
"saving": "Kaydediliyor",
|
||||
"save": "Kaydet",
|
||||
"edit_profile": "Profili Düzenle",
|
||||
"saved_successfully": "Başarıyla kaydedildi",
|
||||
"try_again": "Lütfen tekrar deneyin",
|
||||
"sign_out_modal_title": "Emin misiniz?",
|
||||
"cancel": "İptal",
|
||||
"successfully_signed_out": "Başarıyla çıkış yapıldı",
|
||||
"sign_out": "Çıkış yap",
|
||||
"playing_for": "{{amount}} oynanıyor",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaptığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Çıkışa devam etmek istiyor musunuz?",
|
||||
"add_friends": "Arkadaş Ekle",
|
||||
"add": "Ekle",
|
||||
"friend_code": "Arkadaş kodu",
|
||||
"see_profile": "Profili gör",
|
||||
"sending": "Gönderiliyor",
|
||||
"friend_request_sent": "Arkadaşlık isteği gönderildi",
|
||||
"friends": "Arkadaşlar",
|
||||
"friends_list": "Arkadaş listesi",
|
||||
"user_not_found": "Kullanıcı bulunamadı",
|
||||
"block_user": "Kullanıcıyı engelle",
|
||||
"add_friend": "Arkadaş ekle",
|
||||
"request_sent": "İstek gönderildi",
|
||||
"request_received": "İstek alındı",
|
||||
"accept_request": "İsteği kabul et",
|
||||
"ignore_request": "İsteği yok say",
|
||||
"cancel_request": "İsteği iptal et",
|
||||
"undo_friendship": "Arkadaşlığı sonlandır",
|
||||
"request_accepted": "İstek kabul edildi",
|
||||
"user_blocked_successfully": "Kullanıcı başarıyla engellendi",
|
||||
"user_block_modal_text": "Bu işlem {{displayName}} adlı kullanıcıyı engelleyecek",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"unblock": "Engeli kaldır",
|
||||
"no_friends_added": "Hiç arkadaş eklemediniz",
|
||||
"pending": "Bekliyor",
|
||||
"no_pending_invites": "Bekleyen davetiniz yok",
|
||||
"no_blocked_users": "Engellenmiş kullanıcı yok",
|
||||
"friend_code_copied": "Arkadaş kodu kopyalandı",
|
||||
"undo_friendship_modal_text": "Bu işlem {{displayName}} ile arkadaşlığınızı sonlandıracak",
|
||||
"privacy_hint": "Bunu kimin görebileceğini ayarlamak için <0>Ayarlar</0> bölümüne gidin",
|
||||
"locked_profile": "Bu profil gizli",
|
||||
"image_process_failure": "Görüntü işleme başarısız oldu",
|
||||
"required_field": "Bu alan gerekli",
|
||||
"displayname_min_length": "Görünen isim en az 3 karakter uzunluğunda olmalıdır",
|
||||
"displayname_max_length": "Görünen isim en fazla 50 karakter uzunluğunda olabilir",
|
||||
"report_profile": "Bu profili bildir",
|
||||
"report_reason": "Bu profili neden bildiriyorsunuz?",
|
||||
"report_description": "Ek bilgi",
|
||||
"report_description_placeholder": "Ek bilgi",
|
||||
"report": "Bildir",
|
||||
"report_reason_hate": "Nefret söylemi",
|
||||
"report_reason_sexual_content": "Cinsel içerik",
|
||||
"report_reason_violence": "Şiddet",
|
||||
"report_reason_spam": "Spam",
|
||||
"report_reason_other": "Diğer",
|
||||
"profile_reported": "Profil bildirildi",
|
||||
"your_friend_code": "Arkadaş kodunuz:",
|
||||
"upload_banner": "Afiş yükle",
|
||||
"uploading_banner": "Afiş yükleniyor…",
|
||||
"background_image_updated": "Arka plan görüntüsü güncellendi",
|
||||
"stats": "İstatistikler",
|
||||
"achievements": "Başarılar",
|
||||
"games": "Oyunlar",
|
||||
"top_percentile": "En üst {{percentile}}%",
|
||||
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
|
||||
"playing": "{{game}} oynanıyor",
|
||||
"achievements_unlocked": "Başarılar açıldı",
|
||||
"earned_points": "Kazanılan puanlar",
|
||||
"show_achievements_on_profile": "Başarılarınızı profilinizde gösterin",
|
||||
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Başarı açıldı",
|
||||
"user_achievements": "{{displayName}}'in Başarıları",
|
||||
"your_achievements": "Başarılarınız",
|
||||
"unlocked_at": "Açılma zamanı: {{date}}",
|
||||
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarı açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarı açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
|
||||
"achievement_earn_points": "Bu başarı ile {{points}} puan kazanın",
|
||||
"earned_points": "Kazanılan puanlar:",
|
||||
"available_points": "Mevcut puanlar:",
|
||||
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud Aboneliği",
|
||||
"subscribe_now": "Şimdi abone olun",
|
||||
"cloud_saving": "Bulut kaydetme",
|
||||
"cloud_achievements": "Başarılarınızı buluta kaydedin",
|
||||
"animated_profile_picture": "Animasyonlu profil resimleri",
|
||||
"premium_support": "Premium Destek",
|
||||
"show_and_compare_achievements": "Başarılarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
|
||||
"animated_profile_banner": "Animasyonlu profil afişi",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
|
||||
"learn_more": "Daha Fazla Bilgi Edinin"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
if (!auth) return null;
|
||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||
|
||||
if (!payload) return null;
|
||||
|
||||
return payload.sessionId;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import i18next from "i18next";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
import { HydraApi, WindowManager } from "@main/services";
|
||||
import { AuthPage } from "@shared";
|
||||
|
||||
const openAuthWindow = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
WindowManager.openAuthWindow();
|
||||
const openAuthWindow = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
page: AuthPage
|
||||
) => {
|
||||
const searchParams = new URLSearchParams({
|
||||
lng: i18next.language,
|
||||
});
|
||||
|
||||
if ([AuthPage.UpdateEmail, AuthPage.UpdatePassword].includes(page)) {
|
||||
const { accessToken } = await HydraApi.refreshToken().catch(() => {
|
||||
return { accessToken: "" };
|
||||
});
|
||||
searchParams.set("token", accessToken);
|
||||
}
|
||||
|
||||
WindowManager.openAuthWindow(page, searchParams);
|
||||
};
|
||||
|
||||
registerEvent("openAuthWindow", openAuthWindow);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameArtifact, GameShop } from "@types";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { SubscriptionRequiredError, UserNotLoggedInError } from "@shared";
|
||||
|
||||
const getGameArtifacts = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -22,6 +22,10 @@ const getGameArtifacts = async (
|
||||
return [];
|
||||
}
|
||||
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
15
src/main/events/hardware/check-folder-write-permission.ts
Normal file
15
src/main/events/hardware/check-folder-write-permission.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const checkFolderWritePermission = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
path: string
|
||||
) =>
|
||||
new Promise((resolve) => {
|
||||
fs.access(path, fs.constants.W_OK, (err) => {
|
||||
resolve(!err);
|
||||
});
|
||||
});
|
||||
|
||||
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
||||
@@ -1,10 +1,10 @@
|
||||
import checkDiskSpace from "check-disk-space";
|
||||
import disk from "diskusage";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDiskFreeSpace = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
path: string
|
||||
) => checkDiskSpace(path);
|
||||
) => disk.check(path);
|
||||
|
||||
registerEvent("getDiskFreeSpace", getDiskFreeSpace);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export const parseLaunchOptions = (params: string | null): string[] => {
|
||||
if (params == null || params == "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const paramsSplit = params.split(" ");
|
||||
|
||||
return paramsSplit;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import "./catalogue/get-trending-games";
|
||||
import "./catalogue/get-publishers";
|
||||
import "./catalogue/get-developers";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
@@ -27,9 +28,12 @@ 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 "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
import "./misc/get-features";
|
||||
import "./misc/show-item-in-folder";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./torrenting/resume-game-download";
|
||||
@@ -71,7 +75,6 @@ import "./cloud-save/delete-game-artifact";
|
||||
import "./cloud-save/select-game-backup-path";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
import "./misc/show-item-in-folder";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => appVersion);
|
||||
|
||||
@@ -2,9 +2,7 @@ import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { shell } from "electron";
|
||||
import { spawn } from "child_process";
|
||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||
import { parseLaunchOptions } from "../helpers/parse-launch-options";
|
||||
|
||||
const openGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -12,21 +10,15 @@ const openGame = async (
|
||||
executablePath: string,
|
||||
launchOptions: string | null
|
||||
) => {
|
||||
// TODO: revisit this for launchOptions
|
||||
const parsedPath = parseExecutablePath(executablePath);
|
||||
const parsedParams = parseLaunchOptions(launchOptions);
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: gameId },
|
||||
{ executablePath: parsedPath, launchOptions }
|
||||
);
|
||||
|
||||
if (process.platform === "linux" || process.platform === "darwin") {
|
||||
shell.openPath(parsedPath);
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
spawn(parsedPath, parsedParams, { shell: false, detached: true });
|
||||
}
|
||||
shell.openPath(parsedPath);
|
||||
};
|
||||
|
||||
registerEvent("openGame", openGame);
|
||||
|
||||
56
src/main/events/library/reset-game-achievements.ts
Normal file
56
src/main/events/library/reset-game-achievements.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
||||
import fs from "fs";
|
||||
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
||||
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
||||
|
||||
const resetGameAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
) => {
|
||||
try {
|
||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||
|
||||
if (!game) return;
|
||||
|
||||
const achievementFiles = findAchievementFiles(game);
|
||||
|
||||
if (achievementFiles.length) {
|
||||
for (const achievementFile of achievementFiles) {
|
||||
achievementsLogger.log(`deleting ${achievementFile.filePath}`);
|
||||
await fs.promises.rm(achievementFile.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
await gameAchievementRepository.update(
|
||||
{ objectId: game.objectID },
|
||||
{
|
||||
unlockedAchievements: null,
|
||||
}
|
||||
);
|
||||
|
||||
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
|
||||
() =>
|
||||
achievementsLogger.log(
|
||||
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
|
||||
)
|
||||
);
|
||||
|
||||
const gameAchievements = await getUnlockedAchievements(
|
||||
game.objectID,
|
||||
game.shop,
|
||||
true
|
||||
);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${game.objectID}-${game.shop}`,
|
||||
gameAchievements
|
||||
);
|
||||
} catch (error) {
|
||||
achievementsLogger.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("resetGameAchievements", resetGameAchievements);
|
||||
8
src/main/events/misc/get-features.ts
Normal file
8
src/main/events/misc/get-features.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
|
||||
};
|
||||
|
||||
registerEvent("getFeatures", getFeatures);
|
||||
@@ -1,16 +1,10 @@
|
||||
import { shell } from "electron";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
userAuthRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const [userAuth, userPreferences] = await Promise.all([
|
||||
userAuthRepository.findOne({ where: { id: 1 } }),
|
||||
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||
]);
|
||||
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
|
||||
if (!userAuth) {
|
||||
return;
|
||||
@@ -22,7 +16,6 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
|
||||
const params = new URLSearchParams({
|
||||
token: paymentToken,
|
||||
lng: userPreferences?.language || "en",
|
||||
});
|
||||
|
||||
shell.openExternal(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@main/repository";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { DownloadProgress } from "@types";
|
||||
import { GofileApi, QiwiApi } from "../hosters";
|
||||
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
|
||||
import { PythonRPC } from "../python-rpc";
|
||||
import {
|
||||
LibtorrentPayload,
|
||||
@@ -277,6 +277,16 @@ export class DownloadManager {
|
||||
save_path: game.downloadPath!,
|
||||
};
|
||||
}
|
||||
case Downloader.Datanodes: {
|
||||
const downloadUrl = await DatanodesApi.getDownloadUrl(game.uri!);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
url: downloadUrl,
|
||||
save_path: game.downloadPath!,
|
||||
};
|
||||
}
|
||||
case Downloader.Torrent:
|
||||
return {
|
||||
action: "start",
|
||||
|
||||
47
src/main/services/hosters/datanodes.ts
Normal file
47
src/main/services/hosters/datanodes.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
|
||||
export class DatanodesApi {
|
||||
private static readonly session = axios.create({});
|
||||
|
||||
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/");
|
||||
|
||||
const fileCode = decodeURIComponent(pathSegments[1]);
|
||||
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
rand: "",
|
||||
referer: "https://datanodes.to/download",
|
||||
method_free: "Free Download >>",
|
||||
method_premium: "",
|
||||
adblock_detected: "",
|
||||
});
|
||||
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
|
||||
Host: "datanodes.to",
|
||||
Origin: "https://datanodes.to",
|
||||
Referer: "https://datanodes.to/download",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./gofile";
|
||||
export * from "./qiwi";
|
||||
export * from "./datanodes";
|
||||
|
||||
@@ -215,38 +215,42 @@ export class HydraApi {
|
||||
}
|
||||
}
|
||||
|
||||
public static async refreshToken() {
|
||||
const { accessToken, expiresIn } = await this.instance
|
||||
.post<{ accessToken: string; expiresIn: number }>(`/auth/refresh`, {
|
||||
refreshToken: this.userAuth.refreshToken,
|
||||
})
|
||||
.then((response) => response.data);
|
||||
|
||||
const tokenExpirationTimestamp =
|
||||
Date.now() +
|
||||
this.secondsToMilliseconds(expiresIn) -
|
||||
this.EXPIRATION_OFFSET_IN_MS;
|
||||
|
||||
this.userAuth.authToken = accessToken;
|
||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||
|
||||
logger.log(
|
||||
"Token refreshed. New expiration:",
|
||||
this.userAuth.expirationTimestamp
|
||||
);
|
||||
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
accessToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
return { accessToken, expiresIn };
|
||||
}
|
||||
|
||||
private static async revalidateAccessTokenIfExpired() {
|
||||
const now = new Date();
|
||||
|
||||
if (this.userAuth.expirationTimestamp < now.getTime()) {
|
||||
if (this.userAuth.expirationTimestamp < Date.now()) {
|
||||
try {
|
||||
const response = await this.instance.post(`/auth/refresh`, {
|
||||
refreshToken: this.userAuth.refreshToken,
|
||||
});
|
||||
|
||||
const { accessToken, expiresIn } = response.data;
|
||||
|
||||
const tokenExpirationTimestamp =
|
||||
now.getTime() +
|
||||
this.secondsToMilliseconds(expiresIn) -
|
||||
this.EXPIRATION_OFFSET_IN_MS;
|
||||
|
||||
this.userAuth.authToken = accessToken;
|
||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||
|
||||
logger.log(
|
||||
"Token refreshed. New expiration:",
|
||||
this.userAuth.expirationTimestamp
|
||||
);
|
||||
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
accessToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
await this.refreshToken();
|
||||
} catch (err) {
|
||||
this.handleUnauthorizedError(err);
|
||||
}
|
||||
@@ -261,7 +265,7 @@ export class HydraApi {
|
||||
};
|
||||
}
|
||||
|
||||
private static handleUnauthorizedError = (err) => {
|
||||
private static readonly handleUnauthorizedError = (err) => {
|
||||
if (err instanceof AxiosError && err.response?.status === 401) {
|
||||
logger.error(
|
||||
"401 - Current credentials:",
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
shell,
|
||||
} from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import i18next, { t } from "i18next";
|
||||
import { t } from "i18next";
|
||||
import path from "node:path";
|
||||
import icon from "@resources/icon.png?asset";
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
@@ -17,6 +17,7 @@ import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import UserAgent from "user-agents";
|
||||
import { AuthPage } from "@shared";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
@@ -64,7 +65,10 @@ export class WindowManager {
|
||||
|
||||
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
(details, callback) => {
|
||||
if (details.webContentsId !== this.mainWindow?.webContents.id) {
|
||||
if (
|
||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||
details.url.includes("chatwoot")
|
||||
) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
@@ -81,15 +85,11 @@ export class WindowManager {
|
||||
|
||||
this.mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
(details, callback) => {
|
||||
if (details.webContentsId !== this.mainWindow?.webContents.id) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
if (details.url.includes("featurebase")) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
if (details.url.includes("chatwoot")) {
|
||||
if (
|
||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||
details.url.includes("featurebase") ||
|
||||
details.url.includes("chatwoot")
|
||||
) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export class WindowManager {
|
||||
});
|
||||
}
|
||||
|
||||
public static openAuthWindow() {
|
||||
public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
|
||||
if (this.mainWindow) {
|
||||
const authWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
@@ -165,12 +165,8 @@ export class WindowManager {
|
||||
|
||||
if (!app.isPackaged) authWindow.webContents.openDevTools();
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
lng: i18next.language,
|
||||
});
|
||||
|
||||
authWindow.loadURL(
|
||||
`${import.meta.env.MAIN_VITE_AUTH_URL}/?${searchParams.toString()}`
|
||||
`${import.meta.env.MAIN_VITE_AUTH_URL}${page}?${searchParams.toString()}`
|
||||
);
|
||||
|
||||
authWindow.once("ready-to-show", () => {
|
||||
@@ -182,6 +178,13 @@ export class WindowManager {
|
||||
authWindow.close();
|
||||
|
||||
HydraApi.handleExternalAuth(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.startsWith("hydralauncher://update-account")) {
|
||||
authWindow.close();
|
||||
|
||||
WindowManager.mainWindow?.webContents.send("on-account-updated");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
SeedingStatus,
|
||||
GameAchievement,
|
||||
} from "@types";
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
@@ -130,6 +130,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("deleteGameFolder", gameId),
|
||||
getGameByObjectId: (objectId: string) =>
|
||||
ipcRenderer.invoke("getGameByObjectId", objectId),
|
||||
resetGameAchievements: (gameId: number) =>
|
||||
ipcRenderer.invoke("resetGameAchievements", gameId),
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
@@ -150,6 +152,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) =>
|
||||
ipcRenderer.invoke("getDiskFreeSpace", path),
|
||||
checkFolderWritePermission: (path: string) =>
|
||||
ipcRenderer.invoke("checkFolderWritePermission", path),
|
||||
|
||||
/* Cloud save */
|
||||
uploadSaveGame: (
|
||||
@@ -226,6 +230,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("showOpenDialog", options),
|
||||
showItemInFolder: (path: string) =>
|
||||
ipcRenderer.invoke("showItemInFolder", path),
|
||||
getFeatures: () => ipcRenderer.invoke("getFeatures"),
|
||||
platform: process.platform,
|
||||
|
||||
/* Auto update */
|
||||
@@ -286,13 +291,19 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* Auth */
|
||||
signOut: () => ipcRenderer.invoke("signOut"),
|
||||
openAuthWindow: () => ipcRenderer.invoke("openAuthWindow"),
|
||||
openAuthWindow: (page: AuthPage) =>
|
||||
ipcRenderer.invoke("openAuthWindow", page),
|
||||
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
|
||||
onSignIn: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-signin", listener);
|
||||
return () => ipcRenderer.removeListener("on-signin", listener);
|
||||
},
|
||||
onAccountUpdated: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-account-updated", listener);
|
||||
return () => ipcRenderer.removeListener("on-account-updated", listener);
|
||||
},
|
||||
onSignOut: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-signout", listener);
|
||||
|
||||
@@ -46,6 +46,12 @@ export function Modal({
|
||||
}, [onClose]);
|
||||
|
||||
const isTopMostModal = () => {
|
||||
if (
|
||||
document.querySelector(
|
||||
".featurebase-widget-overlay.featurebase-display-block"
|
||||
)
|
||||
)
|
||||
return false;
|
||||
const openModals = document.querySelectorAll("[role=dialog]");
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { Avatar } from "../avatar/avatar";
|
||||
import { AuthPage } from "@shared";
|
||||
|
||||
const LONG_POLLING_INTERVAL = 120_000;
|
||||
|
||||
@@ -26,11 +27,11 @@ export function SidebarProfile() {
|
||||
|
||||
const handleProfileClick = () => {
|
||||
if (userDetails === null) {
|
||||
window.electron.openAuthWindow();
|
||||
window.electron.openAuthWindow(AuthPage.SignIn);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/profile/${userDetails!.id}`);
|
||||
navigate(`/profile/${userDetails.id}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useId, useMemo, useState } from "react";
|
||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||
import type { FieldError, FieldErrorsImpl, Merge } from "react-hook-form";
|
||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -23,7 +22,7 @@ export interface TextFieldProps
|
||||
HTMLDivElement
|
||||
>;
|
||||
rightContent?: React.ReactNode | null;
|
||||
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
|
||||
error?: string | React.ReactNode;
|
||||
}
|
||||
|
||||
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
@@ -55,10 +54,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
}, [props.type, isPasswordVisible]);
|
||||
|
||||
const hintContent = useMemo(() => {
|
||||
if (error && error.message)
|
||||
return (
|
||||
<small className={styles.errorLabel}>{error.message as string}</small>
|
||||
);
|
||||
if (error) return <small className={styles.errorLabel}>{error}</small>;
|
||||
|
||||
if (hint) return <small>{hint}</small>;
|
||||
return null;
|
||||
|
||||
@@ -8,6 +8,7 @@ export const DOWNLOADER_NAME = {
|
||||
[Downloader.Gofile]: "Gofile",
|
||||
[Downloader.PixelDrain]: "PixelDrain",
|
||||
[Downloader.Qiwi]: "Qiwi",
|
||||
[Downloader.Datanodes]: "Datanodes",
|
||||
};
|
||||
|
||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
@@ -117,11 +117,7 @@ export function GameDetailsContextProvider({
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
window.electron
|
||||
.getGameShopDetails(
|
||||
objectId!,
|
||||
shop as GameShop,
|
||||
getSteamLanguage(i18n.language)
|
||||
)
|
||||
.getGameShopDetails(objectId, shop, getSteamLanguage(i18n.language))
|
||||
.then((result) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
@@ -140,14 +136,14 @@ export function GameDetailsContextProvider({
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
window.electron.getGameStats(objectId, shop as GameShop).then((result) => {
|
||||
window.electron.getGameStats(objectId, shop).then((result) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
setStats(result);
|
||||
});
|
||||
|
||||
if (userDetails) {
|
||||
window.electron
|
||||
.getUnlockedAchievements(objectId, shop as GameShop)
|
||||
.getUnlockedAchievements(objectId, shop)
|
||||
.then((achievements) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
setAchievements(achievements);
|
||||
|
||||
13
src/renderer/src/declaration.d.ts
vendored
13
src/renderer/src/declaration.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||
import type {
|
||||
AppUpdaterEvent,
|
||||
Game,
|
||||
@@ -31,7 +31,7 @@ import type {
|
||||
CatalogueSearchPayload,
|
||||
} from "@types";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
import type disk from "diskusage";
|
||||
|
||||
declare global {
|
||||
declare module "*.svg" {
|
||||
@@ -122,7 +122,7 @@ declare global {
|
||||
) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
|
||||
resetGameAchievements: (gameId: number) => Promise<void>;
|
||||
/* User preferences */
|
||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||
updateUserPreferences: (
|
||||
@@ -140,7 +140,8 @@ declare global {
|
||||
) => Promise<{ fingerprint: string }>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
getDiskFreeSpace: (path: string) => Promise<disk.DiskUsage>;
|
||||
checkFolderWritePermission: (path: string) => Promise<boolean>;
|
||||
|
||||
/* Cloud save */
|
||||
uploadSaveGame: (
|
||||
@@ -195,6 +196,7 @@ declare global {
|
||||
options: Electron.OpenDialogOptions
|
||||
) => Promise<Electron.OpenDialogReturnValue>;
|
||||
showItemInFolder: (path: string) => Promise<void>;
|
||||
getFeatures: () => Promise<string[]>;
|
||||
platform: NodeJS.Platform;
|
||||
|
||||
/* Auto update */
|
||||
@@ -206,9 +208,10 @@ declare global {
|
||||
|
||||
/* Auth */
|
||||
signOut: () => Promise<void>;
|
||||
openAuthWindow: () => Promise<void>;
|
||||
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||
getSessionHash: () => Promise<string | null>;
|
||||
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
|
||||
/* User */
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from "./redux";
|
||||
export * from "./use-user-details";
|
||||
export * from "./use-format";
|
||||
export * from "./use-repacks";
|
||||
export * from "./use-feature";
|
||||
|
||||
23
src/renderer/src/hooks/use-feature.ts
Normal file
23
src/renderer/src/hooks/use-feature.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
enum Feature {
|
||||
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
|
||||
}
|
||||
|
||||
export function useFeature() {
|
||||
useEffect(() => {
|
||||
window.electron.getFeatures().then((features) => {
|
||||
localStorage.setItem("features", JSON.stringify(features || []));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isFeatureEnabled = (feature: Feature) => {
|
||||
const features = JSON.parse(localStorage.getItem("features") || "[]");
|
||||
return features.includes(feature);
|
||||
};
|
||||
|
||||
return {
|
||||
isFeatureEnabled,
|
||||
Feature,
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
UpdateProfileRequest,
|
||||
UserDetails,
|
||||
} from "@types";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
|
||||
@@ -30,6 +31,8 @@ export function useUserDetails() {
|
||||
} = useAppSelector((state) => state.userDetails);
|
||||
|
||||
const clearUserDetails = useCallback(async () => {
|
||||
Sentry.setUser(null);
|
||||
|
||||
dispatch(setUserDetails(null));
|
||||
dispatch(setProfileBackground(null));
|
||||
|
||||
@@ -44,6 +47,12 @@ export function useUserDetails() {
|
||||
|
||||
const updateUserDetails = useCallback(
|
||||
async (userDetails: UserDetails) => {
|
||||
Sentry.setUser({
|
||||
id: userDetails.id,
|
||||
username: userDetails.username,
|
||||
email: userDetails.email ?? undefined,
|
||||
});
|
||||
|
||||
dispatch(setUserDetails(userDetails));
|
||||
window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
|
||||
},
|
||||
|
||||
@@ -31,11 +31,11 @@ export function GameItem({ game }: GameItemProps) {
|
||||
|
||||
const genres = useMemo(() => {
|
||||
return game.genres?.map((genre) => {
|
||||
const index = steamGenres["en"].findIndex(
|
||||
const index = steamGenres["en"]?.findIndex(
|
||||
(steamGenre) => steamGenre === genre
|
||||
);
|
||||
|
||||
if (steamGenres[language] && steamGenres[language][index]) {
|
||||
if (index && steamGenres[language] && steamGenres[language][index]) {
|
||||
return steamGenres[language][index];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Sidebar } from "./sidebar/sidebar";
|
||||
import * as styles from "./game-details.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { AuthPage, steamUrlBuilder } from "@shared";
|
||||
|
||||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
@@ -69,7 +69,7 @@ export function GameDetailsContent() {
|
||||
});
|
||||
|
||||
const backgroundColor = output
|
||||
? (new Color(output).darken(0.7).toString() as string)
|
||||
? new Color(output).darken(0.7).toString()
|
||||
: "";
|
||||
|
||||
setGameColor(backgroundColor);
|
||||
@@ -101,7 +101,7 @@ export function GameDetailsContent() {
|
||||
|
||||
const handleCloudSaveButtonClick = () => {
|
||||
if (!userDetails) {
|
||||
window.electron.openAuthWindow();
|
||||
window.electron.openAuthWindow(AuthPage.SignIn);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,3 +36,10 @@ export const downloaderIcon = style({
|
||||
position: "absolute",
|
||||
left: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const pathError = style({
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { DiskSpace } from "check-disk-space";
|
||||
import * as styles from "./download-settings-modal.css";
|
||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||
@@ -10,7 +9,7 @@ import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||
import type { GameRepack } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||
import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
|
||||
|
||||
export interface DownloadSettingsModalProps {
|
||||
visible: boolean;
|
||||
@@ -33,21 +32,45 @@ export function DownloadSettingsModal({
|
||||
|
||||
const { showErrorToast } = useToast();
|
||||
|
||||
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
|
||||
const [diskFreeSpace, setDiskFreeSpace] = useState<number | null>(null);
|
||||
const [selectedPath, setSelectedPath] = useState("");
|
||||
const [downloadStarting, setDownloadStarting] = useState(false);
|
||||
const [selectedDownloader, setSelectedDownloader] =
|
||||
useState<Downloader | null>(null);
|
||||
const [hasWritePermission, setHasWritePermission] = useState<boolean | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { isFeatureEnabled, Feature } = useFeature();
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const getDiskFreeSpace = (path: string) => {
|
||||
window.electron.getDiskFreeSpace(path).then((result) => {
|
||||
setDiskFreeSpace(result.free);
|
||||
});
|
||||
};
|
||||
|
||||
const checkFolderWritePermission = useCallback(
|
||||
async (path: string) => {
|
||||
if (isFeatureEnabled(Feature.CheckDownloadWritePermission)) {
|
||||
const result = await window.electron.checkFolderWritePermission(path);
|
||||
setHasWritePermission(result);
|
||||
} else {
|
||||
setHasWritePermission(true);
|
||||
}
|
||||
},
|
||||
[Feature, isFeatureEnabled]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
getDiskFreeSpace(selectedPath);
|
||||
checkFolderWritePermission(selectedPath);
|
||||
}
|
||||
}, [visible, selectedPath]);
|
||||
}, [visible, checkFolderWritePermission, selectedPath]);
|
||||
|
||||
const downloaders = useMemo(() => {
|
||||
return getDownloadersForUris(repack?.uris ?? []);
|
||||
@@ -84,12 +107,6 @@ export function DownloadSettingsModal({
|
||||
userPreferences?.realDebridApiToken,
|
||||
]);
|
||||
|
||||
const getDiskFreeSpace = (path: string) => {
|
||||
window.electron.getDiskFreeSpace(path).then((result) => {
|
||||
setDiskFreeSpace(result);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChooseDownloadsPath = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
defaultPath: selectedPath,
|
||||
@@ -124,7 +141,7 @@ export function DownloadSettingsModal({
|
||||
visible={visible}
|
||||
title={t("download_settings")}
|
||||
description={t("space_left_on_disk", {
|
||||
space: formatBytes(diskFreeSpace?.free ?? 0),
|
||||
space: formatBytes(diskFreeSpace ?? 0),
|
||||
})}
|
||||
onClose={onClose}
|
||||
>
|
||||
@@ -168,23 +185,32 @@ export function DownloadSettingsModal({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div className={styles.downloadsPathField}>
|
||||
<TextField
|
||||
value={selectedPath}
|
||||
readOnly
|
||||
disabled
|
||||
label={t("download_path")}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
theme="outline"
|
||||
onClick={handleChooseDownloadsPath}
|
||||
disabled={downloadStarting}
|
||||
>
|
||||
{t("change")}
|
||||
</Button>
|
||||
</div>
|
||||
<TextField
|
||||
value={selectedPath}
|
||||
readOnly
|
||||
disabled
|
||||
label={t("download_path")}
|
||||
error={
|
||||
hasWritePermission === false ? (
|
||||
<span
|
||||
className={styles.pathError}
|
||||
data-open-article="cannot-write-directory"
|
||||
>
|
||||
{t("no_write_permission")}
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
rightContent={
|
||||
<Button
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
theme="outline"
|
||||
onClick={handleChooseDownloadsPath}
|
||||
disabled={downloadStarting}
|
||||
>
|
||||
{t("change")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<p className={styles.hintText}>
|
||||
<Trans i18nKey="select_folder_hint" ns="game_details">
|
||||
@@ -195,7 +221,11 @@ export function DownloadSettingsModal({
|
||||
|
||||
<Button
|
||||
onClick={handleStartClick}
|
||||
disabled={downloadStarting || selectedDownloader === null}
|
||||
disabled={
|
||||
downloadStarting ||
|
||||
selectedDownloader === null ||
|
||||
!hasWritePermission
|
||||
}
|
||||
>
|
||||
<DownloadIcon />
|
||||
{t("download_now")}
|
||||
|
||||
@@ -5,8 +5,9 @@ import type { Game } from "@types";
|
||||
import * as styles from "./game-options-modal.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||
import { useDownload, useToast } from "@renderer/hooks";
|
||||
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||
import { debounce } from "lodash-es";
|
||||
|
||||
@@ -25,12 +26,20 @@ export function GameOptionsModal({
|
||||
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const { updateGame, setShowRepacksModal, repacks, selectGameExecutable } =
|
||||
useContext(gameDetailsContext);
|
||||
const {
|
||||
updateGame,
|
||||
setShowRepacksModal,
|
||||
repacks,
|
||||
selectGameExecutable,
|
||||
achievements,
|
||||
} = useContext(gameDetailsContext);
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
||||
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
|
||||
const [showResetAchievementsModal, setShowResetAchievementsModal] =
|
||||
useState(false);
|
||||
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
|
||||
|
||||
const {
|
||||
removeGameInstaller,
|
||||
@@ -39,6 +48,12 @@ export function GameOptionsModal({
|
||||
cancelDownload,
|
||||
} = useDownload();
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
|
||||
const hasAchievements =
|
||||
(achievements?.filter((achievement) => achievement.unlocked).length ?? 0) >
|
||||
0;
|
||||
|
||||
const deleting = isGameDeleting(game.id);
|
||||
|
||||
const { lastPacket } = useDownload();
|
||||
@@ -141,8 +156,20 @@ export function GameOptionsModal({
|
||||
const shouldShowWinePrefixConfiguration =
|
||||
window.electron.platform === "linux";
|
||||
|
||||
const shouldShowLaunchOptionsConfiguration =
|
||||
window.electron.platform === "win32";
|
||||
const handleResetAchievements = async () => {
|
||||
setIsDeletingAchievements(true);
|
||||
try {
|
||||
await window.electron.resetGameAchievements(game.id);
|
||||
await updateGame();
|
||||
showSuccessToast(t("reset_achievements_success"));
|
||||
} catch (error) {
|
||||
showErrorToast(t("reset_achievements_error"));
|
||||
} finally {
|
||||
setIsDeletingAchievements(false);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowLaunchOptionsConfiguration = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -159,6 +186,13 @@ export function GameOptionsModal({
|
||||
game={game}
|
||||
/>
|
||||
|
||||
<ResetAchievementsModal
|
||||
visible={showResetAchievementsModal}
|
||||
onClose={() => setShowResetAchievementsModal(false)}
|
||||
resetAchievements={handleResetAchievements}
|
||||
game={game}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={game.title}
|
||||
@@ -314,6 +348,20 @@ export function GameOptionsModal({
|
||||
>
|
||||
{t("remove_from_library")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowResetAchievementsModal(true)}
|
||||
theme="danger"
|
||||
disabled={
|
||||
deleting ||
|
||||
isDeletingAchievements ||
|
||||
!hasAchievements ||
|
||||
!userDetails
|
||||
}
|
||||
>
|
||||
{t("reset_achievements")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowDeleteModal(true);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./remove-from-library-modal.css";
|
||||
import type { Game } from "@types";
|
||||
type ResetAchievementsModalProps = Readonly<{
|
||||
visible: boolean;
|
||||
game: Game;
|
||||
onClose: () => void;
|
||||
resetAchievements: () => Promise<void>;
|
||||
}>;
|
||||
|
||||
export function ResetAchievementsModal({
|
||||
onClose,
|
||||
game,
|
||||
visible,
|
||||
resetAchievements,
|
||||
}: ResetAchievementsModalProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const handleResetAchievements = async () => {
|
||||
try {
|
||||
await resetAchievements();
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
title={t("reset_achievements_title")}
|
||||
description={t("reset_achievements_description", {
|
||||
game: game.title,
|
||||
})}
|
||||
>
|
||||
<div className={styles.deleteActionsButtonsCtn}>
|
||||
<Button onClick={handleResetAchievements} theme="outline">
|
||||
{t("reset_achievements")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} theme="primary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChevronDownIcon } from "@primer/octicons-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import * as styles from "./sidebar-section.css";
|
||||
|
||||
@@ -11,6 +11,15 @@ export interface SidebarSectionProps {
|
||||
export function SidebarSection({ title, children }: SidebarSectionProps) {
|
||||
const content = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (content.current && content.current.scrollHeight !== height) {
|
||||
setHeight(isOpen ? content.current.scrollHeight : 0);
|
||||
} else if (!isOpen) {
|
||||
setHeight(0);
|
||||
}
|
||||
}, [isOpen, children, height]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -26,7 +35,7 @@ export function SidebarSection({ title, children }: SidebarSectionProps) {
|
||||
<div
|
||||
ref={content}
|
||||
style={{
|
||||
maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0",
|
||||
maxHeight: `${height}px`,
|
||||
overflow: "hidden",
|
||||
transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)",
|
||||
position: "relative",
|
||||
|
||||
@@ -163,7 +163,7 @@ export function EditProfileModal(
|
||||
minLength={3}
|
||||
maxLength={50}
|
||||
containerProps={{ style: { width: "100%" } }}
|
||||
error={errors.displayName}
|
||||
error={errors.displayName?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export function ReportProfile() {
|
||||
{...register("description")}
|
||||
label={t("report_description")}
|
||||
placeholder={t("report_description_placeholder")}
|
||||
error={errors.description}
|
||||
error={errors.description?.message}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -150,7 +150,7 @@ export function AddDownloadSourceModal({
|
||||
{...register("url")}
|
||||
label={t("download_source_url")}
|
||||
placeholder={t("insert_valid_json_url")}
|
||||
error={errors.url}
|
||||
error={errors.url?.message}
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -5,14 +5,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
export const form = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const blockedUserAvatar = style({
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
borderRadius: "4px",
|
||||
filter: "grayscale(100%)",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
});
|
||||
|
||||
export const blockedUser = style({
|
||||
@@ -43,5 +36,4 @@ export const blockedUsersList = style({
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
marginTop: `${SPACING_UNIT}px`,
|
||||
});
|
||||
291
src/renderer/src/pages/settings/settings-account.tsx
Normal file
291
src/renderer/src/pages/settings/settings-account.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import { Avatar, Button, SelectField } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./settings-account.css";
|
||||
import { useDate, useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
CloudIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
XCircleFillIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
import { AuthPage } from "@shared";
|
||||
|
||||
interface FormValues {
|
||||
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
||||
}
|
||||
|
||||
export function SettingsAccount() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const [isUnblocking, setIsUnblocking] = useState(false);
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext);
|
||||
|
||||
const { formatDate } = useDate();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
setValue,
|
||||
handleSubmit,
|
||||
} = useForm<FormValues>();
|
||||
|
||||
const {
|
||||
userDetails,
|
||||
hasActiveSubscription,
|
||||
patchUser,
|
||||
fetchUserDetails,
|
||||
updateUserDetails,
|
||||
unblockUser,
|
||||
} = useUserDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (userDetails?.profileVisibility) {
|
||||
setValue("profileVisibility", userDetails.profileVisibility);
|
||||
}
|
||||
}, [userDetails, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onAccountUpdated(() => {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
}
|
||||
});
|
||||
showSuccessToast(t("account_data_updated_successfully"));
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [fetchUserDetails, updateUserDetails]);
|
||||
|
||||
const visibilityOptions = [
|
||||
{ value: "PUBLIC", label: t("public") },
|
||||
{ value: "FRIENDS", label: t("friends_only") },
|
||||
{ value: "PRIVATE", label: t("private") },
|
||||
];
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
await patchUser(values);
|
||||
showSuccessToast(t("changes_saved"));
|
||||
};
|
||||
|
||||
const handleUnblockClick = useCallback(
|
||||
(id: string) => {
|
||||
setIsUnblocking(true);
|
||||
|
||||
unblockUser(id)
|
||||
.then(() => {
|
||||
fetchBlockedUsers();
|
||||
showSuccessToast(t("user_unblocked"));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUnblocking(false);
|
||||
});
|
||||
},
|
||||
[unblockUser, fetchBlockedUsers, t, showSuccessToast]
|
||||
);
|
||||
|
||||
const getHydraCloudSectionContent = () => {
|
||||
const hasSubscribedBefore = Boolean(userDetails?.subscription?.expiresAt);
|
||||
const isRenewalActive = userDetails?.subscription?.status === "active";
|
||||
|
||||
if (!hasSubscribedBefore) {
|
||||
return {
|
||||
description: <small>{t("no_subscription")}</small>,
|
||||
callToAction: t("become_subscriber"),
|
||||
};
|
||||
}
|
||||
|
||||
if (hasActiveSubscription) {
|
||||
return {
|
||||
description: isRenewalActive ? (
|
||||
<>
|
||||
<small>
|
||||
{t("subscription_renews_on", {
|
||||
date: formatDate(userDetails.subscription!.expiresAt!),
|
||||
})}
|
||||
</small>
|
||||
<small>{t("bill_sent_until")}</small>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<small>{t("subscription_renew_cancelled")}</small>
|
||||
<small>
|
||||
{t("subscription_active_until", {
|
||||
date: formatDate(userDetails!.subscription!.expiresAt!),
|
||||
})}
|
||||
</small>
|
||||
</>
|
||||
),
|
||||
callToAction: t("manage_subscription"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
description: (
|
||||
<small>
|
||||
{t("subscription_expired_at", {
|
||||
date: formatDate(userDetails!.subscription!.expiresAt!),
|
||||
})}
|
||||
</small>
|
||||
),
|
||||
callToAction: t("renew_subscription"),
|
||||
};
|
||||
};
|
||||
|
||||
if (!userDetails) return null;
|
||||
|
||||
return (
|
||||
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="profileVisibility"
|
||||
render={({ field }) => {
|
||||
const handleChange = (
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
) => {
|
||||
field.onChange(event);
|
||||
handleSubmit(onSubmit)();
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SelectField
|
||||
label={t("profile_visibility")}
|
||||
value={field.value}
|
||||
onChange={handleChange}
|
||||
options={visibilityOptions.map((visiblity) => ({
|
||||
key: visiblity.value,
|
||||
value: visiblity.value,
|
||||
label: visiblity.label,
|
||||
}))}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<small>{t("profile_visibility_description")}</small>
|
||||
</section>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<h4>{t("current_email")}</h4>
|
||||
<p>{userDetails?.email ?? t("no_email_account")}</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
marginTop: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => window.electron.openAuthWindow(AuthPage.UpdateEmail)}
|
||||
>
|
||||
<MailIcon />
|
||||
{t("update_email")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() =>
|
||||
window.electron.openAuthWindow(AuthPage.UpdatePassword)
|
||||
}
|
||||
>
|
||||
<KeyIcon />
|
||||
{t("update_password")}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<h3>Hydra Cloud</h3>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{getHydraCloudSectionContent().description}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
placeSelf: "flex-start",
|
||||
}}
|
||||
theme="outline"
|
||||
onClick={() => window.electron.openCheckout()}
|
||||
>
|
||||
<CloudIcon />
|
||||
{getHydraCloudSectionContent().callToAction}
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<h3>{t("blocked_users")}</h3>
|
||||
|
||||
{blockedUsers.length > 0 ? (
|
||||
<ul className={styles.blockedUsersList}>
|
||||
{blockedUsers.map((user) => {
|
||||
return (
|
||||
<li key={user.id} className={styles.blockedUser}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
style={{ filter: "grayscale(100%)" }}
|
||||
size={32}
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
<span>{user.displayName}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.unblockButton}
|
||||
onClick={() => handleUnblockClick(user.id)}
|
||||
disabled={isUnblocking}
|
||||
>
|
||||
<XCircleFillIcon />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<small>{t("no_users_blocked")}</small>
|
||||
)}
|
||||
</section>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import { SelectField } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./settings-privacy.css";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { XCircleFillIcon } from "@primer/octicons-react";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
|
||||
interface FormValues {
|
||||
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
||||
}
|
||||
|
||||
export function SettingsPrivacy() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const [isUnblocking, setIsUnblocking] = useState(false);
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext);
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
setValue,
|
||||
handleSubmit,
|
||||
} = useForm<FormValues>();
|
||||
|
||||
const { patchUser, userDetails } = useUserDetails();
|
||||
|
||||
const { unblockUser } = useUserDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (userDetails?.profileVisibility) {
|
||||
setValue("profileVisibility", userDetails.profileVisibility);
|
||||
}
|
||||
}, [userDetails, setValue]);
|
||||
|
||||
const visibilityOptions = [
|
||||
{ value: "PUBLIC", label: t("public") },
|
||||
{ value: "FRIENDS", label: t("friends_only") },
|
||||
{ value: "PRIVATE", label: t("private") },
|
||||
];
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
await patchUser(values);
|
||||
showSuccessToast(t("changes_saved"));
|
||||
};
|
||||
|
||||
const handleUnblockClick = useCallback(
|
||||
(id: string) => {
|
||||
setIsUnblocking(true);
|
||||
|
||||
unblockUser(id)
|
||||
.then(() => {
|
||||
fetchBlockedUsers();
|
||||
showSuccessToast(t("user_unblocked"));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUnblocking(false);
|
||||
});
|
||||
},
|
||||
[unblockUser, fetchBlockedUsers, t, showSuccessToast]
|
||||
);
|
||||
|
||||
return (
|
||||
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="profileVisibility"
|
||||
render={({ field }) => {
|
||||
const handleChange = (
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
) => {
|
||||
field.onChange(event);
|
||||
handleSubmit(onSubmit)();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectField
|
||||
label={t("profile_visibility")}
|
||||
value={field.value}
|
||||
onChange={handleChange}
|
||||
options={visibilityOptions.map((visiblity) => ({
|
||||
key: visiblity.value,
|
||||
value: visiblity.value,
|
||||
label: visiblity.label,
|
||||
}))}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<small>{t("profile_visibility_description")}</small>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<h3 style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
|
||||
{t("blocked_users")}
|
||||
</h3>
|
||||
|
||||
<ul className={styles.blockedUsersList}>
|
||||
{blockedUsers.map((user) => {
|
||||
return (
|
||||
<li key={user.id} className={styles.blockedUser}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={user.profileImageUrl!}
|
||||
alt={user.displayName}
|
||||
className={styles.blockedUserAvatar}
|
||||
/>
|
||||
<span>{user.displayName}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.unblockButton}
|
||||
onClick={() => handleUnblockClick(user.id)}
|
||||
disabled={isUnblocking}
|
||||
>
|
||||
<XCircleFillIcon />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SettingsContextConsumer,
|
||||
SettingsContextProvider,
|
||||
} from "@renderer/context";
|
||||
import { SettingsPrivacy } from "./settings-privacy";
|
||||
import { SettingsAccount } from "./settings-account";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Settings() {
|
||||
"Real-Debrid",
|
||||
];
|
||||
|
||||
if (userDetails) return [...categories, t("privacy")];
|
||||
if (userDetails) return [...categories, t("account")];
|
||||
return categories;
|
||||
}, [userDetails, t]);
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function Settings() {
|
||||
return <SettingsRealDebrid />;
|
||||
}
|
||||
|
||||
return <SettingsPrivacy />;
|
||||
return <SettingsAccount />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum Downloader {
|
||||
Gofile,
|
||||
PixelDrain,
|
||||
Qiwi,
|
||||
Datanodes,
|
||||
}
|
||||
|
||||
export enum DownloadSourceStatus {
|
||||
@@ -41,3 +42,9 @@ export enum Cracker {
|
||||
rle = "RLE",
|
||||
razor1911 = "RAZOR1911",
|
||||
}
|
||||
|
||||
export enum AuthPage {
|
||||
SignIn = "/",
|
||||
UpdateEmail = "/update-email",
|
||||
UpdatePassword = "/update-password",
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export const getDownloadersForUri = (uri: string) => {
|
||||
|
||||
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
|
||||
if (uri.startsWith("https://qiwi.gg")) return [Downloader.Qiwi];
|
||||
if (uri.startsWith("https://datanodes.to")) return [Downloader.Datanodes];
|
||||
|
||||
if (realDebridHosts.some((host) => uri.startsWith(host)))
|
||||
return [Downloader.RealDebrid];
|
||||
|
||||
23
yarn.lock
23
yarn.lock
@@ -4381,11 +4381,6 @@ chalk@^5.3.0:
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
|
||||
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
|
||||
|
||||
check-disk-space@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.4.0.tgz#eb8e69eee7a378fd12e35281b8123a8b4c4a8ff7"
|
||||
integrity sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==
|
||||
|
||||
chokidar@^3.5.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
@@ -4921,6 +4916,14 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
diskusage@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/diskusage/-/diskusage-1.2.0.tgz#3e8ae42333d5d7e0c7d93e055d7fea9ea841bc88"
|
||||
integrity sha512-2u3OG3xuf5MFyzc4MctNRUKjjwK+UkovRYdD2ed/NZNZPrt0lqHnLKxGhlFVvAb4/oufIgQG3nWgwmeTbHOvXA==
|
||||
dependencies:
|
||||
es6-promise "^4.2.8"
|
||||
nan "^2.18.0"
|
||||
|
||||
dmg-builder@25.1.8:
|
||||
version "25.1.8"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.8.tgz#41f3b725edd896156e891016a44129e1bd580430"
|
||||
@@ -5327,6 +5330,11 @@ es6-error@^4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
|
||||
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
|
||||
|
||||
es6-promise@^4.2.8:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
esbuild@^0.21.3, esbuild@^0.21.5:
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
||||
@@ -7425,6 +7433,11 @@ mz@^2.4.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nan@^2.18.0:
|
||||
version "2.22.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3"
|
||||
integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==
|
||||
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
|
||||
Reference in New Issue
Block a user