mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: removing crypto from level
This commit is contained in:
commit
0a37ce4cda
128 changed files with 1883 additions and 660 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,3 +14,5 @@ aria2/
|
||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.env.sentry-build-plugin
|
.env.sentry-build-plugin
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
|
|
@ -88,9 +88,8 @@
|
||||||
"@swc/core": "^1.4.16",
|
"@swc/core": "^1.4.16",
|
||||||
"@types/auto-launch": "^5.0.5",
|
"@types/auto-launch": "^5.0.5",
|
||||||
"@types/color": "^3.0.6",
|
"@types/color": "^3.0.6",
|
||||||
"@types/folder-hash": "^4.0.4",
|
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.8",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"@types/parse-torrent": "^5.8.7",
|
"@types/parse-torrent": "^5.8.7",
|
||||||
|
@ -99,12 +98,12 @@
|
||||||
"@types/sound-play": "^1.1.3",
|
"@types/sound-play": "^1.1.3",
|
||||||
"@types/user-agents": "^1.0.4",
|
"@types/user-agents": "^1.0.4",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^31.7.6",
|
"electron": "^31.7.7",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.3.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
|
|
@ -11,11 +11,12 @@ class HttpDownloader:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_download(self, url: str, save_path: str, header: str):
|
def start_download(self, url: str, save_path: str, header: str, out: str = None):
|
||||||
if self.download:
|
if self.download:
|
||||||
self.aria2.resume([self.download])
|
self.aria2.resume([self.download])
|
||||||
else:
|
else:
|
||||||
downloads = self.aria2.add(url, options={"header": header, "dir": save_path})
|
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
|
||||||
|
|
||||||
self.download = downloads[0]
|
self.download = downloads[0]
|
||||||
|
|
||||||
def pause_download(self):
|
def pause_download(self):
|
||||||
|
|
|
@ -28,14 +28,14 @@ if start_download_payload:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[initial_download['game_id']] = torrent_downloader
|
downloads[initial_download['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'], "")
|
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting torrent download", e)
|
print("Error starting torrent download", e)
|
||||||
else:
|
else:
|
||||||
http_downloader = HttpDownloader()
|
http_downloader = HttpDownloader()
|
||||||
downloads[initial_download['game_id']] = http_downloader
|
downloads[initial_download['game_id']] = http_downloader
|
||||||
try:
|
try:
|
||||||
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'))
|
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting http download", e)
|
print("Error starting http download", e)
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ if start_seeding_payload:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||||
downloads[seed['game_id']] = torrent_downloader
|
downloads[seed['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
torrent_downloader.start_download(seed['url'], seed['save_path'], "")
|
torrent_downloader.start_download(seed['url'], seed['save_path'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error starting seeding", e)
|
print("Error starting seeding", e)
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ def seed_status():
|
||||||
|
|
||||||
@app.route("/healthcheck", methods=["GET"])
|
@app.route("/healthcheck", methods=["GET"])
|
||||||
def healthcheck():
|
def healthcheck():
|
||||||
return "", 200
|
return "ok", 200
|
||||||
|
|
||||||
@app.route("/process-list", methods=["GET"])
|
@app.route("/process-list", methods=["GET"])
|
||||||
def process_list():
|
def process_list():
|
||||||
|
@ -140,18 +140,18 @@ def action():
|
||||||
|
|
||||||
if url.startswith('magnet'):
|
if url.startswith('magnet'):
|
||||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'], "")
|
existing_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[game_id] = torrent_downloader
|
downloads[game_id] = torrent_downloader
|
||||||
torrent_downloader.start_download(url, data['save_path'], "")
|
torrent_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
else:
|
else:
|
||||||
http_downloader = HttpDownloader()
|
http_downloader = HttpDownloader()
|
||||||
downloads[game_id] = http_downloader
|
downloads[game_id] = http_downloader
|
||||||
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
|
|
||||||
downloading_game_id = game_id
|
downloading_game_id = game_id
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ def action():
|
||||||
elif action == 'resume_seeding':
|
elif action == 'resume_seeding':
|
||||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||||
downloads[game_id] = torrent_downloader
|
downloads[game_id] = torrent_downloader
|
||||||
torrent_downloader.start_download(data['url'], data['save_path'], "")
|
torrent_downloader.start_download(data['url'], data['save_path'])
|
||||||
elif action == 'pause_seeding':
|
elif action == 'pause_seeding':
|
||||||
downloader = downloads.get(game_id)
|
downloader = downloads.get(game_id)
|
||||||
if downloader:
|
if downloader:
|
||||||
|
|
|
@ -102,7 +102,7 @@ class TorrentDownloader:
|
||||||
"http://bvarf.tracker.sh:2086/announce",
|
"http://bvarf.tracker.sh:2086/announce",
|
||||||
]
|
]
|
||||||
|
|
||||||
def start_download(self, magnet: str, save_path: str, header: str):
|
def start_download(self, magnet: str, save_path: str):
|
||||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags}
|
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags}
|
||||||
self.torrent_handle = self.session.add_torrent(params)
|
self.torrent_handle = self.session.add_torrent(params)
|
||||||
self.torrent_handle.resume()
|
self.torrent_handle.resume()
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -236,13 +236,13 @@
|
||||||
"behavior": "السلوك",
|
"behavior": "السلوك",
|
||||||
"download_sources": "مصادر التنزيل",
|
"download_sources": "مصادر التنزيل",
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"real_debrid_api_token": "رمز API",
|
"api_token": "رمز API",
|
||||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
||||||
"real_debrid_invalid_token": "رمز API غير صالح",
|
"debrid_invalid_token": "رمز API غير صالح",
|
||||||
"real_debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||||
"real_debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||||
"save_changes": "حفظ التغييرات",
|
"save_changes": "حفظ التغييرات",
|
||||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
"paused": "{{title}} (Спынена)",
|
"paused": "{{title}} (Спынена)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
||||||
"filter": "Фільтар бібліятэкі",
|
"filter": "Фільтар бібліятэкі",
|
||||||
"home": "Галоўная"
|
"home": "Галоўная",
|
||||||
|
"favorites": "Улюбленыя"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Пошук",
|
"search": "Пошук",
|
||||||
"home": "Галоўная",
|
"home": "Галоўная",
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
"game_has_no_executable": "Играта няма избран изпълним файл",
|
"game_has_no_executable": "Играта няма избран изпълним файл",
|
||||||
"sign_in": "Вписване",
|
"sign_in": "Вписване",
|
||||||
"friends": "Приятели",
|
"friends": "Приятели",
|
||||||
"need_help": "Имате нужда от помощ??"
|
"need_help": "Имате нужда от помощ??",
|
||||||
|
"favorites": "Любими игри"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Търсене",
|
"search": "Търсене",
|
||||||
|
@ -230,13 +231,13 @@
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Източници за изтегляне",
|
"download_sources": "Източници за изтегляне",
|
||||||
"language": "Език",
|
"language": "Език",
|
||||||
"real_debrid_api_token": "API Токен",
|
"api_token": "API Токен",
|
||||||
"enable_real_debrid": "Включи Real-Debrid",
|
"enable_real_debrid": "Включи Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
|
||||||
"real_debrid_invalid_token": "Невалиден API токен",
|
"debrid_invalid_token": "Невалиден API токен",
|
||||||
"real_debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
"debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
|
||||||
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
|
||||||
"real_debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
"debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
|
||||||
"save_changes": "Запази промените",
|
"save_changes": "Запази промените",
|
||||||
"changes_saved": "Промените са успешно запазни",
|
"changes_saved": "Промените са успешно запазни",
|
||||||
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
|
||||||
|
|
|
@ -20,10 +20,12 @@
|
||||||
"home": "Inici",
|
"home": "Inici",
|
||||||
"queued": "{{title}} (En espera)",
|
"queued": "{{title}} (En espera)",
|
||||||
"game_has_no_executable": "El joc encara no té un executable seleccionat",
|
"game_has_no_executable": "El joc encara no té un executable seleccionat",
|
||||||
"sign_in": "Entra"
|
"sign_in": "Entra",
|
||||||
|
"favorites": "Favorits"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cerca jocs",
|
"search": "Cerca jocs",
|
||||||
|
|
||||||
"home": "Inici",
|
"home": "Inici",
|
||||||
"catalogue": "Catàleg",
|
"catalogue": "Catàleg",
|
||||||
"downloads": "Baixades",
|
"downloads": "Baixades",
|
||||||
|
@ -161,13 +163,13 @@
|
||||||
"behavior": "Comportament",
|
"behavior": "Comportament",
|
||||||
"download_sources": "Fonts de descàrrega",
|
"download_sources": "Fonts de descàrrega",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Testimoni API",
|
"api_token": "Testimoni API",
|
||||||
"enable_real_debrid": "Activa el Real Debrid",
|
"enable_real_debrid": "Activa el Real Debrid",
|
||||||
"real_debrid_description": "Real-Debrid és un programa de descàrrega sense restriccions que us permet descarregar fitxers a l'instant i al màxim de la vostra velocitat d'Internet.",
|
"real_debrid_description": "Real-Debrid és un programa de descàrrega sense restriccions que us permet descarregar fitxers a l'instant i al màxim de la vostra velocitat d'Internet.",
|
||||||
"real_debrid_invalid_token": "Invalida el testimoni de l'API",
|
"debrid_invalid_token": "Invalida el testimoni de l'API",
|
||||||
"real_debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí</0>.",
|
"debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí</0>.",
|
||||||
"real_debrid_free_account_error": "L'usuari \"{{username}}\" és un compte gratuït. Si us plau subscriu-te a Real-Debrid",
|
"real_debrid_free_account_error": "L'usuari \"{{username}}\" és un compte gratuït. Si us plau subscriu-te a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Compte \"{{username}}\" vinculat",
|
"debrid_linked_message": "Compte \"{{username}}\" vinculat",
|
||||||
"save_changes": "Desa els canvis",
|
"save_changes": "Desa els canvis",
|
||||||
"changes_saved": "Els canvis s'han desat correctament",
|
"changes_saved": "Els canvis s'han desat correctament",
|
||||||
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
|
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
|
||||||
"sign_in": "Přihlásit se",
|
"sign_in": "Přihlásit se",
|
||||||
"friends": "Přátelé",
|
"friends": "Přátelé",
|
||||||
"need_help": "Potřebujete pomoc?"
|
"need_help": "Potřebujete pomoc?",
|
||||||
|
"favorites": "Oblíbené"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Vyhledat hry",
|
"search": "Vyhledat hry",
|
||||||
|
@ -214,13 +215,13 @@
|
||||||
"behavior": "Chování",
|
"behavior": "Chování",
|
||||||
"download_sources": "Zdroje stahování",
|
"download_sources": "Zdroje stahování",
|
||||||
"language": "Jazyk",
|
"language": "Jazyk",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Povolit Real-Debrid",
|
"enable_real_debrid": "Povolit Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid je neomezený správce stahování, který umožňuje stahovat soubory v nejvyšší rychlosti vašeho internetu.",
|
"real_debrid_description": "Real-Debrid je neomezený správce stahování, který umožňuje stahovat soubory v nejvyšší rychlosti vašeho internetu.",
|
||||||
"real_debrid_invalid_token": "Neplatný API token",
|
"debrid_invalid_token": "Neplatný API token",
|
||||||
"real_debrid_api_token_hint": "API token můžeš sehnat <0>zde</0>",
|
"debrid_api_token_hint": "API token můžeš sehnat <0>zde</0>",
|
||||||
"real_debrid_free_account_error": "Účet \"{{username}}\" má základní úroveň. Prosím předplaťte si Real-Debrid",
|
"real_debrid_free_account_error": "Účet \"{{username}}\" má základní úroveň. Prosím předplaťte si Real-Debrid",
|
||||||
"real_debrid_linked_message": "Účet \"{{username}}\" je propojen",
|
"debrid_linked_message": "Účet \"{{username}}\" je propojen",
|
||||||
"save_changes": "Uložit změny",
|
"save_changes": "Uložit změny",
|
||||||
"changes_saved": "Změny úspěšně uloženy",
|
"changes_saved": "Změny úspěšně uloženy",
|
||||||
"download_sources_description": "Hydra bude odsud sbírat soubory. Zdrojový odkaz musí být .json soubor obsahující odkazy na soubory.",
|
"download_sources_description": "Hydra bude odsud sbírat soubory. Zdrojový odkaz musí být .json soubor obsahující odkazy na soubory.",
|
||||||
|
|
|
@ -24,10 +24,12 @@
|
||||||
"queued": "{{title}} (I køen)",
|
"queued": "{{title}} (I køen)",
|
||||||
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
|
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
|
||||||
"sign_in": "Log ind",
|
"sign_in": "Log ind",
|
||||||
"friends": "Venner"
|
"friends": "Venner",
|
||||||
|
"favorites": "Favoritter"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søg efter spil",
|
"search": "Søg efter spil",
|
||||||
|
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
|
@ -177,13 +179,13 @@
|
||||||
"behavior": "Opførsel",
|
"behavior": "Opførsel",
|
||||||
"download_sources": "Download kilder",
|
"download_sources": "Download kilder",
|
||||||
"language": "Sprog",
|
"language": "Sprog",
|
||||||
"real_debrid_api_token": "API nøgle",
|
"api_token": "API nøgle",
|
||||||
"enable_real_debrid": "Slå Real-Debrid til",
|
"enable_real_debrid": "Slå Real-Debrid til",
|
||||||
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
||||||
"real_debrid_invalid_token": "Ugyldig API nøgle",
|
"debrid_invalid_token": "Ugyldig API nøgle",
|
||||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
"debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
||||||
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
||||||
"real_debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
"debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
||||||
"save_changes": "Gem ændringer",
|
"save_changes": "Gem ændringer",
|
||||||
"changes_saved": "Ændringer gemt successfuldt",
|
"changes_saved": "Ændringer gemt successfuldt",
|
||||||
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
||||||
|
|
|
@ -20,10 +20,12 @@
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"queued": "{{title}} (In Warteschlange)",
|
"queued": "{{title}} (In Warteschlange)",
|
||||||
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
||||||
"sign_in": "Anmelden"
|
"sign_in": "Anmelden",
|
||||||
|
"favorites": "Favoriten"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Spiele suchen",
|
"search": "Spiele suchen",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
|
@ -161,13 +163,13 @@
|
||||||
"behavior": "Verhalten",
|
"behavior": "Verhalten",
|
||||||
"download_sources": "Download-Quellen",
|
"download_sources": "Download-Quellen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Real-Debrid aktivieren",
|
"enable_real_debrid": "Real-Debrid aktivieren",
|
||||||
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
|
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
|
||||||
"real_debrid_invalid_token": "API token nicht gültig",
|
"debrid_invalid_token": "API token nicht gültig",
|
||||||
"real_debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
|
"debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
|
||||||
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
|
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
|
||||||
"real_debrid_linked_message": "Konto \"{{username}}\" verknüpft",
|
"debrid_linked_message": "Konto \"{{username}}\" verknüpft",
|
||||||
"save_changes": "Änderungen speichern",
|
"save_changes": "Änderungen speichern",
|
||||||
"changes_saved": "Änderungen erfolgreich gespeichert",
|
"changes_saved": "Änderungen erfolgreich gespeichert",
|
||||||
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
|
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
"game_has_no_executable": "Game has no executable selected",
|
"game_has_no_executable": "Game has no executable selected",
|
||||||
"sign_in": "Sign in",
|
"sign_in": "Sign in",
|
||||||
"friends": "Friends",
|
"friends": "Friends",
|
||||||
"need_help": "Need help?"
|
"need_help": "Need help?",
|
||||||
|
"favorites": "Favorites"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
|
@ -184,8 +185,13 @@
|
||||||
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
||||||
"reset_achievements_title": "Are you sure?",
|
"reset_achievements_title": "Are you sure?",
|
||||||
"reset_achievements_success": "Achievements successfully reset",
|
"reset_achievements_success": "Achievements successfully reset",
|
||||||
"reset_achievements_error": "Failed to reset achievements"
|
"reset_achievements_error": "Failed to reset achievements",
|
||||||
|
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||||
|
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
|
||||||
},
|
},
|
||||||
|
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
"installation_id": "Installation ID:",
|
"installation_id": "Installation ID:",
|
||||||
|
@ -236,13 +242,13 @@
|
||||||
"behavior": "Behavior",
|
"behavior": "Behavior",
|
||||||
"download_sources": "Download sources",
|
"download_sources": "Download sources",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"real_debrid_api_token": "API Token",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Enable Real-Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to quickly download files, only limited by your internet speed.",
|
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to quickly download files, only limited by your internet speed.",
|
||||||
"real_debrid_invalid_token": "Invalid API token",
|
"debrid_invalid_token": "Invalid API token",
|
||||||
"real_debrid_api_token_hint": "You can get your API token <0>here</0>",
|
"debrid_api_token_hint": "You can get your API token <0>here</0>",
|
||||||
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
||||||
"real_debrid_linked_message": "Account \"{{username}}\" linked",
|
"debrid_linked_message": "Account \"{{username}}\" linked",
|
||||||
"save_changes": "Save changes",
|
"save_changes": "Save changes",
|
||||||
"changes_saved": "Changes successfully saved",
|
"changes_saved": "Changes successfully saved",
|
||||||
"download_sources_description": "Hydra will fetch the download links from these sources. The source URL must be a direct link to a .json file containing the download links.",
|
"download_sources_description": "Hydra will fetch the download links from these sources. The source URL must be a direct link to a .json file containing the download links.",
|
||||||
|
@ -316,7 +322,11 @@
|
||||||
"delete_all_themes_description": "This will delete all your custom themes",
|
"delete_all_themes_description": "This will delete all your custom themes",
|
||||||
"delete_theme_description": "This will delete the theme {{theme}}",
|
"delete_theme_description": "This will delete the theme {{theme}}",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"appearance": "Appearance"
|
"appearance": "Appearance",
|
||||||
|
"enable_torbox": "Enable Torbox",
|
||||||
|
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||||
|
"torbox_account_linked": "TorBox account linked",
|
||||||
|
"real_debrid_account_linked": "Real-Debrid account linked"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||||
"sign_in": "Iniciar sesión",
|
"sign_in": "Iniciar sesión",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "¿Necesitas ayuda?"
|
"need_help": "¿Necesitas ayuda?",
|
||||||
|
"favorites": "Favoritos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar juegos",
|
"search": "Buscar juegos",
|
||||||
|
@ -175,7 +176,16 @@
|
||||||
"backup_from": "Copia de seguridad de {{date}}",
|
"backup_from": "Copia de seguridad de {{date}}",
|
||||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||||
"clear": "Limpiar",
|
"clear": "Limpiar",
|
||||||
"no_directory_selected": "No se seleccionó un directorio"
|
"no_directory_selected": "No se seleccionó un directorio",
|
||||||
|
"launch_options": "Opciones de Inicio",
|
||||||
|
"launch_options_description": "Los usuarios avanzados pueden introducir sus propias modificaciones de opciones de inicio (característica experimental)",
|
||||||
|
"launch_options_placeholder": "Sin parámetro específicado",
|
||||||
|
"no_write_permission": "No se puede descargar en este directorio. Presiona aquí para aprender más.",
|
||||||
|
"reset_achievements": "Reiniciar logros",
|
||||||
|
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||||
|
"reset_achievements_title": "¿Estás seguro?",
|
||||||
|
"reset_achievements_success": "Logros reiniciados exitosamente",
|
||||||
|
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
|
@ -227,13 +237,13 @@
|
||||||
"behavior": "Otros",
|
"behavior": "Otros",
|
||||||
"download_sources": "Fuentes de descarga",
|
"download_sources": "Fuentes de descarga",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Activar Real-Debrid",
|
"enable_real_debrid": "Activar Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
"debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
||||||
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
"debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Guardar cambios",
|
"save_changes": "Guardar cambios",
|
||||||
"changes_saved": "Ajustes guardados exitosamente",
|
"changes_saved": "Ajustes guardados exitosamente",
|
||||||
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
||||||
|
@ -271,7 +281,23 @@
|
||||||
"launch_minimized": "Iniciar Hydra minimizado",
|
"launch_minimized": "Iniciar Hydra minimizado",
|
||||||
"disable_nsfw_alert": "Desactivar alerta NSFW",
|
"disable_nsfw_alert": "Desactivar alerta NSFW",
|
||||||
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
|
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
|
||||||
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos"
|
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos",
|
||||||
|
"account": "Cuenta",
|
||||||
|
"account_data_updated_successfully": "Datos de la cuenta actualizados",
|
||||||
|
"bill_sent_until": "Tú próxima factura se enviará el {{date}}",
|
||||||
|
"current_email": "Correo actual:",
|
||||||
|
"manage_subscription": "Gestionar suscripción",
|
||||||
|
"no_email_account": "No has configurado un correo aún",
|
||||||
|
"no_subscription": "Disfruta Hydra de la mejor manera",
|
||||||
|
"no_users_blocked": "No tienes usuarios bloqueados",
|
||||||
|
"notifications": "Notificaciones",
|
||||||
|
"renew_subscription": "Renovar Hydra Cloud",
|
||||||
|
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
|
||||||
|
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
|
||||||
|
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||||
|
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||||
|
"update_email": "Actualizar correo",
|
||||||
|
"update_password": "Actualizar contraseña"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
|
|
|
@ -25,7 +25,8 @@
|
||||||
"queued": "{{title}} (Järjekorras)",
|
"queued": "{{title}} (Järjekorras)",
|
||||||
"game_has_no_executable": "Mängul pole käivitusfaili valitud",
|
"game_has_no_executable": "Mängul pole käivitusfaili valitud",
|
||||||
"sign_in": "Logi sisse",
|
"sign_in": "Logi sisse",
|
||||||
"friends": "Sõbrad"
|
"friends": "Sõbrad",
|
||||||
|
"favorites": "Lemmikud"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Otsi mänge",
|
"search": "Otsi mänge",
|
||||||
|
@ -213,13 +214,13 @@
|
||||||
"behavior": "Käitumine",
|
"behavior": "Käitumine",
|
||||||
"download_sources": "Allalaadimise allikad",
|
"download_sources": "Allalaadimise allikad",
|
||||||
"language": "Keel",
|
"language": "Keel",
|
||||||
"real_debrid_api_token": "API Võti",
|
"api_token": "API Võti",
|
||||||
"enable_real_debrid": "Luba Real-Debrid",
|
"enable_real_debrid": "Luba Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid on piiranguteta allalaadija, mis võimaldab sul faile alla laadida koheselt ja sinu internetiühenduse parima kiirusega.",
|
"real_debrid_description": "Real-Debrid on piiranguteta allalaadija, mis võimaldab sul faile alla laadida koheselt ja sinu internetiühenduse parima kiirusega.",
|
||||||
"real_debrid_invalid_token": "Vigane API võti",
|
"debrid_invalid_token": "Vigane API võti",
|
||||||
"real_debrid_api_token_hint": "Sa saad oma API võtme <0>siit</0>",
|
"debrid_api_token_hint": "Sa saad oma API võtme <0>siit</0>",
|
||||||
"real_debrid_free_account_error": "Konto \"{{username}}\" on tasuta konto. Palun telli Real-Debrid",
|
"real_debrid_free_account_error": "Konto \"{{username}}\" on tasuta konto. Palun telli Real-Debrid",
|
||||||
"real_debrid_linked_message": "Konto \"{{username}}\" ühendatud",
|
"debrid_linked_message": "Konto \"{{username}}\" ühendatud",
|
||||||
"save_changes": "Salvesta muudatused",
|
"save_changes": "Salvesta muudatused",
|
||||||
"changes_saved": "Muudatused edukalt salvestatud",
|
"changes_saved": "Muudatused edukalt salvestatud",
|
||||||
"download_sources_description": "Hydra laeb allalaadimise lingid nendest allikatest. Allika URL peab olema otsene link .json failile, mis sisaldab allalaadimise linke.",
|
"download_sources_description": "Hydra laeb allalaadimise lingid nendest allikatest. Allika URL peab olema otsene link .json failile, mis sisaldab allalaadimise linke.",
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
"paused": "{{title}} (متوقف شده)",
|
"paused": "{{title}} (متوقف شده)",
|
||||||
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
||||||
"filter": "فیلتر کردن کتابخانه",
|
"filter": "فیلتر کردن کتابخانه",
|
||||||
"home": "خانه"
|
"home": "خانه",
|
||||||
|
"favorites": "علاقهمندیها"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "جستجوی بازیها",
|
"search": "جستجوی بازیها",
|
||||||
"home": "خانه",
|
"home": "خانه",
|
||||||
|
@ -110,7 +112,7 @@
|
||||||
"general": "کلی",
|
"general": "کلی",
|
||||||
"behavior": "رفتار",
|
"behavior": "رفتار",
|
||||||
"enable_real_debrid": "فعالسازی Real-Debrid",
|
"enable_real_debrid": "فعالسازی Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
"debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
||||||
"save_changes": "ذخیره تغییرات"
|
"save_changes": "ذخیره تغییرات"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
"paused": "{{title}} (En pause)",
|
"paused": "{{title}} (En pause)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||||
"filter": "Filtrer la bibliothèque",
|
"filter": "Filtrer la bibliothèque",
|
||||||
"home": "Page d’accueil"
|
"home": "Page d’accueil",
|
||||||
|
"favorites": "Favoris"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Recherche",
|
"search": "Recherche",
|
||||||
|
|
||||||
"catalogue": "Catalogue",
|
"catalogue": "Catalogue",
|
||||||
"downloads": "Téléchargements",
|
"downloads": "Téléchargements",
|
||||||
"search_results": "Résultats de la recherche",
|
"search_results": "Résultats de la recherche",
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
"paused": "{{title}} (Szünet)",
|
"paused": "{{title}} (Szünet)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||||
"filter": "Könyvtár szűrése",
|
"filter": "Könyvtár szűrése",
|
||||||
"home": "Főoldal"
|
"home": "Főoldal",
|
||||||
|
"favorites": "Kedvenc játékok"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
|
|
||||||
"home": "Főoldal",
|
"home": "Főoldal",
|
||||||
"catalogue": "Katalógus",
|
"catalogue": "Katalógus",
|
||||||
"downloads": "Letöltések",
|
"downloads": "Letöltések",
|
||||||
|
|
|
@ -20,10 +20,12 @@
|
||||||
"home": "Beranda",
|
"home": "Beranda",
|
||||||
"queued": "{{title}} (Antrian)",
|
"queued": "{{title}} (Antrian)",
|
||||||
"game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
|
"game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
|
||||||
"sign_in": "Masuk"
|
"sign_in": "Masuk",
|
||||||
|
"favorites": "Favorit"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cari game",
|
"search": "Cari game",
|
||||||
|
|
||||||
"home": "Beranda",
|
"home": "Beranda",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Unduhan",
|
"downloads": "Unduhan",
|
||||||
|
@ -161,13 +163,13 @@
|
||||||
"behavior": "Perilaku",
|
"behavior": "Perilaku",
|
||||||
"download_sources": "Sumber unduhan",
|
"download_sources": "Sumber unduhan",
|
||||||
"language": "Bahasa",
|
"language": "Bahasa",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Aktifkan Real-Debrid",
|
"enable_real_debrid": "Aktifkan Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid adalah downloader tanpa batas yang memungkinkan kamu untuk mengunduh file dengan cepat dan pada kecepatan terbaik dari Internet kamu.",
|
"real_debrid_description": "Real-Debrid adalah downloader tanpa batas yang memungkinkan kamu untuk mengunduh file dengan cepat dan pada kecepatan terbaik dari Internet kamu.",
|
||||||
"real_debrid_invalid_token": "Token API tidak valid",
|
"debrid_invalid_token": "Token API tidak valid",
|
||||||
"real_debrid_api_token_hint": "Kamu bisa dapatkan token API di <0>sini</0>",
|
"debrid_api_token_hint": "Kamu bisa dapatkan token API di <0>sini</0>",
|
||||||
"real_debrid_free_account_error": "Akun \"{{username}}\" adalah akun gratis. Silakan berlangganan Real-Debrid",
|
"real_debrid_free_account_error": "Akun \"{{username}}\" adalah akun gratis. Silakan berlangganan Real-Debrid",
|
||||||
"real_debrid_linked_message": "Akun \"{{username}}\" terhubung",
|
"debrid_linked_message": "Akun \"{{username}}\" terhubung",
|
||||||
"save_changes": "Simpan perubahan",
|
"save_changes": "Simpan perubahan",
|
||||||
"changes_saved": "Perubahan disimpan berhasil",
|
"changes_saved": "Perubahan disimpan berhasil",
|
||||||
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
"paused": "{{title}} (In pausa)",
|
"paused": "{{title}} (In pausa)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||||
"filter": "Filtra libreria",
|
"filter": "Filtra libreria",
|
||||||
"home": "Home"
|
"home": "Home",
|
||||||
|
"favorites": "Preferiti"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Catalogo",
|
"catalogue": "Catalogo",
|
||||||
"downloads": "Download",
|
"downloads": "Download",
|
||||||
|
@ -118,7 +120,7 @@
|
||||||
"general": "Generale",
|
"general": "Generale",
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"enable_real_debrid": "Abilita Real Debrid",
|
"enable_real_debrid": "Abilita Real Debrid",
|
||||||
"real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
"debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
||||||
"save_changes": "Salva modifiche"
|
"save_changes": "Salva modifiche"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -20,8 +20,10 @@
|
||||||
"home": "Басты бет",
|
"home": "Басты бет",
|
||||||
"queued": "{{title}} (Кезекте)",
|
"queued": "{{title}} (Кезекте)",
|
||||||
"game_has_no_executable": "Ойынды іске қосу файлы таңдалмаған",
|
"game_has_no_executable": "Ойынды іске қосу файлы таңдалмаған",
|
||||||
"sign_in": "Кіру"
|
"sign_in": "Кіру",
|
||||||
|
"favorites": "Таңдаулылар"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Іздеу",
|
"search": "Іздеу",
|
||||||
"home": "Басты бет",
|
"home": "Басты бет",
|
||||||
|
@ -159,13 +161,13 @@
|
||||||
"behavior": "Мінез-құлық",
|
"behavior": "Мінез-құлық",
|
||||||
"download_sources": "Жүктеу көздері",
|
"download_sources": "Жүктеу көздері",
|
||||||
"language": "Тіл",
|
"language": "Тіл",
|
||||||
"real_debrid_api_token": "API Кілті",
|
"api_token": "API Кілті",
|
||||||
"enable_real_debrid": "Real-Debrid-ті қосу",
|
"enable_real_debrid": "Real-Debrid-ті қосу",
|
||||||
"real_debrid_description": "Real-Debrid - бұл шектеусіз жүктеуші, ол интернетте орналастырылған файлдарды тез жүктеуге немесе жеке желі арқылы кез келген блоктарды айналып өтіп, оларды бірден плеерге беруге мүмкіндік береді.",
|
"real_debrid_description": "Real-Debrid - бұл шектеусіз жүктеуші, ол интернетте орналастырылған файлдарды тез жүктеуге немесе жеке желі арқылы кез келген блоктарды айналып өтіп, оларды бірден плеерге беруге мүмкіндік береді.",
|
||||||
"real_debrid_invalid_token": "Қате API кілті",
|
"debrid_invalid_token": "Қате API кілті",
|
||||||
"real_debrid_api_token_hint": "API кілтін <0>осы жерден</0> алуға болады",
|
"debrid_api_token_hint": "API кілтін <0>осы жерден</0> алуға болады",
|
||||||
"real_debrid_free_account_error": "\"{{username}}\" аккаунты жазылымға ие емес. Real-Debrid жазылымын алыңыз",
|
"real_debrid_free_account_error": "\"{{username}}\" аккаунты жазылымға ие емес. Real-Debrid жазылымын алыңыз",
|
||||||
"real_debrid_linked_message": "\"{{username}}\" аккаунты байланған",
|
"debrid_linked_message": "\"{{username}}\" аккаунты байланған",
|
||||||
"save_changes": "Өзгерістерді сақтау",
|
"save_changes": "Өзгерістерді сақтау",
|
||||||
"changes_saved": "Өзгерістер сәтті сақталды",
|
"changes_saved": "Өзгерістер сәтті сақталды",
|
||||||
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
"paused": "{{title}} (일시 정지됨)",
|
"paused": "{{title}} (일시 정지됨)",
|
||||||
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
||||||
"filter": "라이브러리 정렬",
|
"filter": "라이브러리 정렬",
|
||||||
"home": "홈"
|
"home": "홈",
|
||||||
|
"favorites": "즐겨찾기"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "게임 검색하기",
|
"search": "게임 검색하기",
|
||||||
"home": "홈",
|
"home": "홈",
|
||||||
|
@ -110,7 +112,7 @@
|
||||||
"general": "일반",
|
"general": "일반",
|
||||||
"behavior": "행동",
|
"behavior": "행동",
|
||||||
"enable_real_debrid": "Real-Debrid 활성화",
|
"enable_real_debrid": "Real-Debrid 활성화",
|
||||||
"real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
"debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
||||||
"save_changes": "변경 사항 저장"
|
"save_changes": "변경 사항 저장"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -24,10 +24,12 @@
|
||||||
"queued": "{{title}} (I køen)",
|
"queued": "{{title}} (I køen)",
|
||||||
"game_has_no_executable": "Spillet har ikke noen kjørbar fil valgt",
|
"game_has_no_executable": "Spillet har ikke noen kjørbar fil valgt",
|
||||||
"sign_in": "Logge inn",
|
"sign_in": "Logge inn",
|
||||||
"friends": "Venner"
|
"friends": "Venner",
|
||||||
|
"favorites": "Favoritter"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søk efter spill",
|
"search": "Søk efter spill",
|
||||||
|
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Nedlastinger",
|
"downloads": "Nedlastinger",
|
||||||
|
@ -177,13 +179,13 @@
|
||||||
"behavior": "Oppførsel",
|
"behavior": "Oppførsel",
|
||||||
"download_sources": "Nedlastingskilder",
|
"download_sources": "Nedlastingskilder",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"real_debrid_api_token": "API nøkkel",
|
"api_token": "API nøkkel",
|
||||||
"enable_real_debrid": "Slå på Real-Debrid",
|
"enable_real_debrid": "Slå på Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid er en ubegrenset nedlaster som gør det mulig for deg å laste ned filer med en gang og med den beste utnyttelsen av internethastigheten din.",
|
"real_debrid_description": "Real-Debrid er en ubegrenset nedlaster som gør det mulig for deg å laste ned filer med en gang og med den beste utnyttelsen av internethastigheten din.",
|
||||||
"real_debrid_invalid_token": "Ugyldig API nøkkel",
|
"debrid_invalid_token": "Ugyldig API nøkkel",
|
||||||
"real_debrid_api_token_hint": "Du kan få API nøkkelen din <0>her</0>",
|
"debrid_api_token_hint": "Du kan få API nøkkelen din <0>her</0>",
|
||||||
"real_debrid_free_account_error": "Brukeren \"{{username}}\" er en gratis bruker. Vennligst abboner på Real-Debrid",
|
"real_debrid_free_account_error": "Brukeren \"{{username}}\" er en gratis bruker. Vennligst abboner på Real-Debrid",
|
||||||
"real_debrid_linked_message": "Brukeren \"{{username}}\" er forbunnet",
|
"debrid_linked_message": "Brukeren \"{{username}}\" er forbunnet",
|
||||||
"save_changes": "Lagre endringer",
|
"save_changes": "Lagre endringer",
|
||||||
"changes_saved": "Lagring av endringer vellykket",
|
"changes_saved": "Lagring av endringer vellykket",
|
||||||
"download_sources_description": "Hydra vil hente nedlastingslenker fra disse kildene. Kilde URLen skal være en direkte lenke til en .json fil som inneholder nedlastingslenkene.",
|
"download_sources_description": "Hydra vil hente nedlastingslenker fra disse kildene. Kilde URLen skal være en direkte lenke til en .json fil som inneholder nedlastingslenkene.",
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
"paused": "{{title}} (Gepauzeerd)",
|
"paused": "{{title}} (Gepauzeerd)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filter Bibliotheek",
|
"filter": "Filter Bibliotheek",
|
||||||
"home": "Home"
|
"home": "Home",
|
||||||
|
"favorites": "Favorieten"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Zoek spellen",
|
"search": "Zoek spellen",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Bibliotheek",
|
"catalogue": "Bibliotheek",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
|
@ -111,7 +113,7 @@
|
||||||
"general": "Algemeen",
|
"general": "Algemeen",
|
||||||
"behavior": "Gedrag",
|
"behavior": "Gedrag",
|
||||||
"enable_real_debrid": "Enable Real-Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
"debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
||||||
"save_changes": "Wijzigingen opslaan"
|
"save_changes": "Wijzigingen opslaan"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
"paused": "{{title}} (Zatrzymano)",
|
"paused": "{{title}} (Zatrzymano)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||||
"filter": "Filtruj biblioteke",
|
"filter": "Filtruj biblioteke",
|
||||||
"home": "Główna"
|
"home": "Główna",
|
||||||
|
"favorites": "Ulubione"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
|
|
||||||
"home": "Główna",
|
"home": "Główna",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Pobrane",
|
"downloads": "Pobrane",
|
||||||
|
@ -119,7 +121,7 @@
|
||||||
"behavior": "Zachowania",
|
"behavior": "Zachowania",
|
||||||
"language": "Język",
|
"language": "Język",
|
||||||
"enable_real_debrid": "Włącz Real-Debrid",
|
"enable_real_debrid": "Włącz Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
"debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
||||||
"save_changes": "Zapisz zmiany"
|
"save_changes": "Zapisz zmiany"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -26,10 +26,12 @@
|
||||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||||
"sign_in": "Login",
|
"sign_in": "Login",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "Precisa de ajuda?"
|
"need_help": "Precisa de ajuda?",
|
||||||
|
"favorites": "Favoritos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
|
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"search_results": "Resultados da busca",
|
"search_results": "Resultados da busca",
|
||||||
|
@ -172,8 +174,14 @@
|
||||||
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
||||||
"reset_achievements_title": "Tem certeza?",
|
"reset_achievements_title": "Tem certeza?",
|
||||||
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
||||||
"reset_achievements_error": "Falha ao resetar conquistas"
|
"reset_achievements_error": "Falha ao resetar conquistas",
|
||||||
|
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais.",
|
||||||
|
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||||
|
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
|
||||||
},
|
},
|
||||||
|
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
"installation_id": "ID da instalação:",
|
"installation_id": "ID da instalação:",
|
||||||
|
@ -224,13 +232,13 @@
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"download_sources": "Fontes de download",
|
"download_sources": "Fontes de download",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token de API",
|
"api_token": "Token de API",
|
||||||
"enable_real_debrid": "Habilitar Real-Debrid",
|
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
"debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
||||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite baixar arquivos instantaneamente e com a melhor velocidade da sua Internet.",
|
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite baixar arquivos instantaneamente e com a melhor velocidade da sua Internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
"debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Salvar mudanças",
|
"save_changes": "Salvar mudanças",
|
||||||
"changes_saved": "Ajustes salvos com sucesso",
|
"changes_saved": "Ajustes salvos com sucesso",
|
||||||
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||||
|
@ -284,7 +292,11 @@
|
||||||
"become_subscriber": "Seja Hydra Cloud",
|
"become_subscriber": "Seja Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
|
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
||||||
|
"enable_torbox": "Habilitar Torbox",
|
||||||
|
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||||
|
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||||
|
"real_debrid_account_linked": "Conta Real-Debrid associada"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
|
|
@ -25,10 +25,12 @@
|
||||||
"queued": "{{title}} (Na fila)",
|
"queued": "{{title}} (Na fila)",
|
||||||
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
||||||
"sign_in": "Iniciar sessão",
|
"sign_in": "Iniciar sessão",
|
||||||
"friends": "Amigos"
|
"friends": "Amigos",
|
||||||
|
"favorites": "Favoritos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Procurar jogos",
|
"search": "Procurar jogos",
|
||||||
|
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
"downloads": "Transferências",
|
"downloads": "Transferências",
|
||||||
"search_results": "Resultados da pesquisa",
|
"search_results": "Resultados da pesquisa",
|
||||||
|
@ -205,13 +207,13 @@
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"download_sources": "Fontes de transferência",
|
"download_sources": "Fontes de transferência",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"real_debrid_api_token": "Token de API",
|
"api_token": "Token de API",
|
||||||
"enable_real_debrid": "Ativar Real-Debrid",
|
"enable_real_debrid": "Ativar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Podes obter o teu token de API <0>aqui</0>",
|
"debrid_api_token_hint": "Podes obter o teu token de API <0>aqui</0>",
|
||||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite descarregar ficheiros instantaneamente e com a melhor velocidade da tua Internet.",
|
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite descarregar ficheiros instantaneamente e com a melhor velocidade da tua Internet.",
|
||||||
"real_debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token de API inválido",
|
||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreve o Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreve o Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" associada",
|
"debrid_linked_message": "Conta \"{{username}}\" associada",
|
||||||
"save_changes": "Guardar alterações",
|
"save_changes": "Guardar alterações",
|
||||||
"changes_saved": "Alterações guardadas com sucesso",
|
"changes_saved": "Alterações guardadas com sucesso",
|
||||||
"download_sources_description": "O Hydra vai procurar links de download em todas as fontes ativadas. O URL da fonte deve ser um link direto para um ficheiro .json que contenha uma lista de links.",
|
"download_sources_description": "O Hydra vai procurar links de download em todas as fontes ativadas. O URL da fonte deve ser um link direto para um ficheiro .json que contenha uma lista de links.",
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
"paused": "{{title}} (Pauzat)",
|
"paused": "{{title}} (Pauzat)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Se descarcă...)",
|
"downloading": "{{title}} ({{percentage}} - Se descarcă...)",
|
||||||
"filter": "Filtrează biblioteca",
|
"filter": "Filtrează biblioteca",
|
||||||
"home": "Acasă"
|
"home": "Acasă",
|
||||||
|
"favorites": "Favorite"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Caută jocuri",
|
"search": "Caută jocuri",
|
||||||
|
|
||||||
"home": "Acasă",
|
"home": "Acasă",
|
||||||
"catalogue": "Catalog",
|
"catalogue": "Catalog",
|
||||||
"downloads": "Descărcări",
|
"downloads": "Descărcări",
|
||||||
|
@ -124,13 +126,13 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Comportament",
|
"behavior": "Comportament",
|
||||||
"language": "Limbă",
|
"language": "Limbă",
|
||||||
"real_debrid_api_token": "Token API",
|
"api_token": "Token API",
|
||||||
"enable_real_debrid": "Activează Real-Debrid",
|
"enable_real_debrid": "Activează Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
|
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
|
||||||
"real_debrid_invalid_token": "Token API invalid",
|
"debrid_invalid_token": "Token API invalid",
|
||||||
"real_debrid_api_token_hint": "Poți obține token-ul tău API <0>aici</0>",
|
"debrid_api_token_hint": "Poți obține token-ul tău API <0>aici</0>",
|
||||||
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
||||||
"real_debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
||||||
"save_changes": "Salvează modificările",
|
"save_changes": "Salvează modificările",
|
||||||
"changes_saved": "Modificările au fost salvate cu succes"
|
"changes_saved": "Modificările au fost salvate cu succes"
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
"game_has_no_executable": "Файл запуска игры не выбран",
|
"game_has_no_executable": "Файл запуска игры не выбран",
|
||||||
"sign_in": "Войти",
|
"sign_in": "Войти",
|
||||||
"friends": "Друзья",
|
"friends": "Друзья",
|
||||||
"need_help": "Нужна помощь?"
|
"need_help": "Нужна помощь?",
|
||||||
|
"favorites": "Избранное"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
|
@ -237,13 +238,13 @@
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Источники загрузки",
|
"download_sources": "Источники загрузки",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"real_debrid_api_token": "API Ключ",
|
"api_token": "API Ключ",
|
||||||
"enable_real_debrid": "Включить Real-Debrid",
|
"enable_real_debrid": "Включить Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы, размещенные в Интернете, или мгновенно передавать их в плеер через частную сеть, позволяющую обходить любые блокировки.",
|
"real_debrid_description": "Real-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы, размещенные в Интернете, или мгновенно передавать их в плеер через частную сеть, позволяющую обходить любые блокировки.",
|
||||||
"real_debrid_invalid_token": "Неверный API ключ",
|
"debrid_invalid_token": "Неверный API ключ",
|
||||||
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
"debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
||||||
"real_debrid_free_account_error": "Аккаунт \"{{username}}\" - не имеет подписки. Пожалуйста, оформите подписку на Real-Debrid",
|
"real_debrid_free_account_error": "Аккаунт \"{{username}}\" - не имеет подписки. Пожалуйста, оформите подписку на Real-Debrid",
|
||||||
"real_debrid_linked_message": "Привязан аккаунт \"{{username}}\"",
|
"debrid_linked_message": "Привязан аккаунт \"{{username}}\"",
|
||||||
"save_changes": "Сохранить изменения",
|
"save_changes": "Сохранить изменения",
|
||||||
"changes_saved": "Изменения успешно сохранены",
|
"changes_saved": "Изменения успешно сохранены",
|
||||||
"download_sources_description": "Hydra будет получать ссылки на загрузки из этих источников. URL должна содержать прямую ссылку на .json-файл с ссылками для загрузок.",
|
"download_sources_description": "Hydra будет получать ссылки на загрузки из этих источников. URL должна содержать прямую ссылку на .json-файл с ссылками для загрузок.",
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
||||||
"sign_in": "Giriş yap",
|
"sign_in": "Giriş yap",
|
||||||
"friends": "Arkadaşlar",
|
"friends": "Arkadaşlar",
|
||||||
"need_help": "Yardıma mı ihtiyacınız var?"
|
"need_help": "Yardıma mı ihtiyacınız var?",
|
||||||
|
"favorites": "Favoriler"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Oyunları ara",
|
"search": "Oyunları ara",
|
||||||
|
@ -236,13 +237,13 @@
|
||||||
"behavior": "Davranış",
|
"behavior": "Davranış",
|
||||||
"download_sources": "İndirme kaynakları",
|
"download_sources": "İndirme kaynakları",
|
||||||
"language": "Dil",
|
"language": "Dil",
|
||||||
"real_debrid_api_token": "API Anahtarı",
|
"api_token": "API Anahtarı",
|
||||||
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
|
"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_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ı",
|
"debrid_invalid_token": "Geçersiz API anahtarı",
|
||||||
"real_debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
|
"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_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
|
||||||
"real_debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
"debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
|
||||||
"save_changes": "Değişiklikleri Kaydet",
|
"save_changes": "Değişiklikleri Kaydet",
|
||||||
"changes_saved": "Değişiklikler başarıyla kaydedildi",
|
"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.",
|
"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.",
|
||||||
|
|
|
@ -20,10 +20,12 @@
|
||||||
"home": "Головна",
|
"home": "Головна",
|
||||||
"game_has_no_executable": "Не було вибрано файл для запуску гри",
|
"game_has_no_executable": "Не було вибрано файл для запуску гри",
|
||||||
"queued": "{{title}} в черзі",
|
"queued": "{{title}} в черзі",
|
||||||
"sign_in": "Увійти"
|
"sign_in": "Увійти",
|
||||||
|
"favorites": "Улюблені"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Пошук",
|
"search": "Пошук",
|
||||||
|
|
||||||
"home": "Головна",
|
"home": "Головна",
|
||||||
"catalogue": "Каталог",
|
"catalogue": "Каталог",
|
||||||
"downloads": "Завантаження",
|
"downloads": "Завантаження",
|
||||||
|
@ -174,13 +176,13 @@
|
||||||
"import": "Імпортувати",
|
"import": "Імпортувати",
|
||||||
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
|
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
|
||||||
"language": "Мова",
|
"language": "Мова",
|
||||||
"real_debrid_api_token": "API-токен",
|
"api_token": "API-токен",
|
||||||
"real_debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
"debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
||||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||||
"real_debrid_description": "Real-Debrid — це необмежений завантажувач, який дозволяє швидко завантажувати файли, розміщені в Інтернеті, або миттєво передавати їх у плеєр через приватну мережу, що дозволяє обходити будь-які блокування.",
|
"real_debrid_description": "Real-Debrid — це необмежений завантажувач, який дозволяє швидко завантажувати файли, розміщені в Інтернеті, або миттєво передавати їх у плеєр через приватну мережу, що дозволяє обходити будь-які блокування.",
|
||||||
"real_debrid_free_account_error": "Акаунт \"{{username}}\" - не має наявної підписки. Будь ласка, оформіть підписку на Real-Debrid",
|
"real_debrid_free_account_error": "Акаунт \"{{username}}\" - не має наявної підписки. Будь ласка, оформіть підписку на Real-Debrid",
|
||||||
"real_debrid_invalid_token": "Невірний API-токен",
|
"debrid_invalid_token": "Невірний API-токен",
|
||||||
"real_debrid_linked_message": "Акаунт \"{{username}}\" привязаний",
|
"debrid_linked_message": "Акаунт \"{{username}}\" привязаний",
|
||||||
"remove_download_source": "Видалити",
|
"remove_download_source": "Видалити",
|
||||||
"removed_download_source": "Джерело завантажень було видалено",
|
"removed_download_source": "Джерело завантажень було видалено",
|
||||||
"save_changes": "Зберегти зміни",
|
"save_changes": "Зберегти зміни",
|
||||||
|
|
|
@ -25,7 +25,8 @@
|
||||||
"queued": "{{title}} (已加入下载队列)",
|
"queued": "{{title}} (已加入下载队列)",
|
||||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||||
"sign_in": "登入",
|
"sign_in": "登入",
|
||||||
"friends": "好友"
|
"friends": "好友",
|
||||||
|
"favorites": "收藏"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "搜索游戏",
|
"search": "搜索游戏",
|
||||||
|
@ -213,13 +214,13 @@
|
||||||
"behavior": "行为",
|
"behavior": "行为",
|
||||||
"download_sources": "下载源",
|
"download_sources": "下载源",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"real_debrid_api_token": "API 令牌",
|
"api_token": "API 令牌",
|
||||||
"enable_real_debrid": "启用 Real-Debrid",
|
"enable_real_debrid": "启用 Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
|
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
|
||||||
"real_debrid_invalid_token": "无效的 API 令牌",
|
"debrid_invalid_token": "无效的 API 令牌",
|
||||||
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
"debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
||||||
"real_debrid_free_account_error": "账户 \"{{username}}\" 是免费账户。请订阅 Real-Debrid",
|
"real_debrid_free_account_error": "账户 \"{{username}}\" 是免费账户。请订阅 Real-Debrid",
|
||||||
"real_debrid_linked_message": "账户 \"{{username}}\" 已链接",
|
"debrid_linked_message": "账户 \"{{username}}\" 已链接",
|
||||||
"save_changes": "保存更改",
|
"save_changes": "保存更改",
|
||||||
"changes_saved": "更改已成功保存",
|
"changes_saved": "更改已成功保存",
|
||||||
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
||||||
import { PythonRPC } from "@main/services/python-rpc";
|
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
@ -25,9 +24,6 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
/* Cancels any ongoing downloads */
|
/* Cancels any ongoing downloads */
|
||||||
DownloadManager.cancelDownload();
|
DownloadManager.cancelDownload();
|
||||||
|
|
||||||
/* Disconnects libtorrent */
|
|
||||||
PythonRPC.kill();
|
|
||||||
|
|
||||||
HydraApi.handleSignOut();
|
HydraApi.handleSignOut();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
|
@ -1,47 +1,8 @@
|
||||||
import type { AppUpdaterEvent } from "@types";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import updater, { UpdateInfo } from "electron-updater";
|
import { UpdateManager } from "@main/services/update-manager";
|
||||||
import { WindowManager } from "@main/services";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
|
||||||
|
|
||||||
const sendEvent = (event: AppUpdaterEvent) => {
|
|
||||||
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendEventsForDebug = false;
|
|
||||||
|
|
||||||
const isAutoInstallAvailable =
|
|
||||||
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
|
|
||||||
|
|
||||||
const mockValuesForDebug = () => {
|
|
||||||
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const newVersionInfo = { version: "" };
|
|
||||||
|
|
||||||
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
autoUpdater
|
return UpdateManager.checkForUpdates();
|
||||||
.once("update-available", (info: UpdateInfo) => {
|
|
||||||
sendEvent({ type: "update-available", info });
|
|
||||||
newVersionInfo.version = info.version;
|
|
||||||
})
|
|
||||||
.once("update-downloaded", () => {
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
publishNotificationUpdateReadyToInstall(newVersionInfo.version);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
autoUpdater.autoDownload = isAutoInstallAvailable;
|
|
||||||
autoUpdater.checkForUpdates();
|
|
||||||
} else if (sendEventsForDebug) {
|
|
||||||
mockValuesForDebug();
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAutoInstallAvailable;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("checkForUpdates", checkForUpdates);
|
registerEvent("checkForUpdates", checkForUpdates);
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const checkFolderWritePermission = async (
|
const checkFolderWritePermission = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
path: string
|
testPath: string
|
||||||
) =>
|
) => {
|
||||||
new Promise((resolve) => {
|
const testFilePath = path.join(testPath, ".hydra-write-test");
|
||||||
fs.access(path, fs.constants.W_OK, (err) => {
|
|
||||||
resolve(!err);
|
try {
|
||||||
});
|
fs.writeFileSync(testFilePath, "");
|
||||||
});
|
fs.rmSync(testFilePath);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
||||||
|
|
|
@ -3,15 +3,14 @@ import { db, levelKeys } from "@main/level";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
export const getDownloadsPath = async () => {
|
export const getDownloadsPath = async () => {
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userPreferences && userPreferences.downloadsPath)
|
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||||
return userPreferences.downloadsPath;
|
|
||||||
|
|
||||||
return defaultDownloadsPath;
|
return defaultDownloadsPath;
|
||||||
};
|
};
|
||||||
|
|
7
src/main/events/helpers/parse-launch-options.ts
Normal file
7
src/main/events/helpers/parse-launch-options.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const parseLaunchOptions = (params?: string | null): string[] => {
|
||||||
|
if (!params) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.split(" ");
|
||||||
|
};
|
|
@ -13,6 +13,8 @@ import "./catalogue/get-developers";
|
||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./hardware/check-folder-write-permission";
|
import "./hardware/check-folder-write-permission";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
|
import "./library/add-game-to-favorites";
|
||||||
|
import "./library/remove-game-from-favorites";
|
||||||
import "./library/create-game-shortcut";
|
import "./library/create-game-shortcut";
|
||||||
import "./library/close-game";
|
import "./library/close-game";
|
||||||
import "./library/delete-game-folder";
|
import "./library/delete-game-folder";
|
||||||
|
@ -46,6 +48,7 @@ import "./user-preferences/auto-launch";
|
||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
import "./user-preferences/authenticate-torbox";
|
||||||
import "./download-sources/put-download-source";
|
import "./download-sources/put-download-source";
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
|
|
25
src/main/events/library/add-game-to-favorites.ts
Normal file
25
src/main/events/library/add-game-to-favorites.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const addGameToFavorites = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
favorite: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to update game favorite status: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("addGameToFavorites", addGameToFavorites);
|
|
@ -46,9 +46,9 @@ const addGameToLibrary = async (
|
||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(game!);
|
updateLocalUnlockedAchivements(game);
|
||||||
|
|
||||||
createGame(game!).catch(() => {});
|
createGame(game).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
|
import { spawn } from "child_process";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { GameShop } from "@types";
|
import { GameShop } from "@types";
|
||||||
|
import { parseLaunchOptions } from "../helpers/parse-launch-options";
|
||||||
|
|
||||||
const openGame = async (
|
const openGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -11,8 +13,8 @@ const openGame = async (
|
||||||
executablePath: string,
|
executablePath: string,
|
||||||
launchOptions?: string | null
|
launchOptions?: string | null
|
||||||
) => {
|
) => {
|
||||||
// TODO: revisit this for launchOptions
|
|
||||||
const parsedPath = parseExecutablePath(executablePath);
|
const parsedPath = parseExecutablePath(executablePath);
|
||||||
|
const parsedParams = parseLaunchOptions(launchOptions);
|
||||||
|
|
||||||
const gameKey = levelKeys.game(shop, objectId);
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
@ -26,7 +28,12 @@ const openGame = async (
|
||||||
launchOptions,
|
launchOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (parsedParams.length === 0) {
|
||||||
shell.openPath(parsedPath);
|
shell.openPath(parsedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(parsedPath, parsedParams, { shell: false, detached: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("openGame", openGame);
|
registerEvent("openGame", openGame);
|
||||||
|
|
25
src/main/events/library/remove-game-from-favorites.ts
Normal file
25
src/main/events/library/remove-game-from-favorites.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const removeGameFromFavorites = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
favorite: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to update game favorite status: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("removeGameFromFavorites", removeGameFromFavorites);
|
|
@ -10,7 +10,7 @@ const publishNewRepacksNotification = async (
|
||||||
) => {
|
) => {
|
||||||
if (newRepacksCount < 1) return;
|
if (newRepacksCount < 1) return;
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { omit } from "lodash-es";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import { fileTypeFromFile } from "file-type";
|
||||||
|
|
||||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
export const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||||
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { Download, StartGameDownloadPayload } from "@types";
|
import type { Download, StartGameDownloadPayload } from "@types";
|
||||||
import { DownloadManager, HydraApi } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { Downloader, DownloadError, steamUrlBuilder } from "@shared";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -75,9 +76,10 @@ const startGameDownload = async (
|
||||||
queued: true,
|
queued: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await downloadsSublevel.put(gameKey, download);
|
try {
|
||||||
|
await DownloadManager.startDownload(download).then(() => {
|
||||||
await DownloadManager.startDownload(download);
|
return downloadsSublevel.put(gameKey, download);
|
||||||
|
});
|
||||||
|
|
||||||
const updatedGame = await gamesSublevel.get(gameKey);
|
const updatedGame = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
@ -92,6 +94,37 @@ const startGameDownload = async (
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
).catch(() => {}),
|
).catch(() => {}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
logger.error("Failed to start download", err);
|
||||||
|
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response?.status === 429 && downloader === Downloader.Gofile) {
|
||||||
|
return { ok: false, error: DownloadError.GofileQuotaExceeded };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
err.response?.status === 403 &&
|
||||||
|
downloader === Downloader.RealDebrid
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: DownloadError.RealDebridAccountNotAuthorized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloader === Downloader.TorBox) {
|
||||||
|
return { ok: false, error: err.response?.data?.detail };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return { ok: false, error: err.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("startGameDownload", startGameDownload);
|
registerEvent("startGameDownload", startGameDownload);
|
||||||
|
|
14
src/main/events/user-preferences/authenticate-torbox.ts
Normal file
14
src/main/events/user-preferences/authenticate-torbox.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { TorBoxClient } from "@main/services/download/torbox";
|
||||||
|
|
||||||
|
const authenticateTorBox = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
apiToken: string
|
||||||
|
) => {
|
||||||
|
TorBoxClient.authorize(apiToken);
|
||||||
|
|
||||||
|
const user = await TorBoxClient.getUser();
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("authenticateTorBox", authenticateTorBox);
|
|
@ -3,12 +3,13 @@ import { registerEvent } from "../register-event";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import { patchUserProfile } from "../profile/update-profile";
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) => {
|
) => {
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
);
|
);
|
||||||
|
@ -19,6 +20,11 @@ const updateUserPreferences = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
i18next.changeLanguage(preferences.language);
|
i18next.changeLanguage(preferences.language);
|
||||||
|
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preferences.downloadsPath) {
|
||||||
|
preferences.downloadsPath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.put<string, UserPreferences>(
|
await db.put<string, UserPreferences>(
|
||||||
|
|
|
@ -10,7 +10,7 @@ const getComparedUnlockedAchievements = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
|
@ -25,7 +25,7 @@ const getComparedUnlockedAchievements = async (
|
||||||
{
|
{
|
||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
language: userPreferences?.language || "en",
|
language: userPreferences?.language ?? "en",
|
||||||
}
|
}
|
||||||
).then((achievements) => {
|
).then((achievements) => {
|
||||||
const sortedAchievements = achievements.achievements
|
const sortedAchievements = achievements.achievements
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const getUnlockedAchievements = async (
|
||||||
levelKeys.game(shop, objectId)
|
levelKeys.game(shop, objectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
|
|
|
@ -9,6 +9,7 @@ import resources from "@locales";
|
||||||
import { PythonRPC } from "./services/python-rpc";
|
import { PythonRPC } from "./services/python-rpc";
|
||||||
import { Aria2 } from "./services/aria2";
|
import { Aria2 } from "./services/aria2";
|
||||||
import { db, levelKeys } from "./level";
|
import { db, levelKeys } from "./level";
|
||||||
|
import { loadState } from "./main";
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
const { autoUpdater } = updater;
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ app.whenReady().then(async () => {
|
||||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
await import("./main");
|
await loadState();
|
||||||
|
|
||||||
const language = await db.get<string, string>(levelKeys.language, {
|
const language = await db.get<string, string>(levelKeys.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf-8",
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export { db } from "./level";
|
export { db } from "./level";
|
||||||
|
|
||||||
export * from "./sublevels";
|
export * from "./sublevels";
|
||||||
|
|
|
@ -2,5 +2,4 @@ export * from "./downloads";
|
||||||
export * from "./games";
|
export * from "./games";
|
||||||
export * from "./game-shop-cache";
|
export * from "./game-shop-cache";
|
||||||
export * from "./game-achievements";
|
export * from "./game-achievements";
|
||||||
|
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
|
|
|
@ -14,9 +14,20 @@ import {
|
||||||
} from "./level";
|
} from "./level";
|
||||||
import { Auth, User, type UserPreferences } from "@types";
|
import { Auth, User, type UserPreferences } from "@types";
|
||||||
import { knexClient } from "./knex-client";
|
import { knexClient } from "./knex-client";
|
||||||
|
import { TorBoxClient } from "./services/download/torbox";
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
export const loadState = async () => {
|
||||||
import("./events");
|
const userPreferences = await migrateFromSqlite().then(async () => {
|
||||||
|
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
return db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await import("./events");
|
||||||
|
|
||||||
Aria2.spawn();
|
Aria2.spawn();
|
||||||
|
|
||||||
|
@ -24,6 +35,10 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.torBoxApiToken) {
|
||||||
|
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||||
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
|
||||||
HydraApi.setupApi().then(() => {
|
HydraApi.setupApi().then(() => {
|
||||||
|
@ -96,7 +111,9 @@ const migrateFromSqlite = async () => {
|
||||||
if (userPreferences.length > 0) {
|
if (userPreferences.length > 0) {
|
||||||
const { realDebridApiToken, ...rest } = userPreferences[0];
|
const { realDebridApiToken, ...rest } = userPreferences[0];
|
||||||
|
|
||||||
await db.put(levelKeys.userPreferences, {
|
await db.put<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{
|
||||||
...rest,
|
...rest,
|
||||||
realDebridApiToken,
|
realDebridApiToken,
|
||||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
|
@ -106,12 +123,15 @@ const migrateFromSqlite = async () => {
|
||||||
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
||||||
showHiddenAchievementsDescription:
|
showHiddenAchievementsDescription:
|
||||||
rest.showHiddenAchievementsDescription === 1,
|
rest.showHiddenAchievementsDescription === 1,
|
||||||
downloadNotificationsEnabled: rest.downloadNotificationsEnabled === 1,
|
downloadNotificationsEnabled:
|
||||||
|
rest.downloadNotificationsEnabled === 1,
|
||||||
repackUpdatesNotificationsEnabled:
|
repackUpdatesNotificationsEnabled:
|
||||||
rest.repackUpdatesNotificationsEnabled === 1,
|
rest.repackUpdatesNotificationsEnabled === 1,
|
||||||
achievementNotificationsEnabled:
|
achievementNotificationsEnabled:
|
||||||
rest.achievementNotificationsEnabled === 1,
|
rest.achievementNotificationsEnabled === 1,
|
||||||
});
|
},
|
||||||
|
{ valueEncoding: "json" }
|
||||||
|
);
|
||||||
|
|
||||||
if (rest.language) {
|
if (rest.language) {
|
||||||
await db.put(levelKeys.language, rest.language);
|
await db.put(levelKeys.language, rest.language);
|
||||||
|
@ -182,15 +202,3 @@ const migrateFromSqlite = async () => {
|
||||||
migrateUser,
|
migrateUser,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
migrateFromSqlite().then(async () => {
|
|
||||||
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
|
||||||
valueEncoding: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
|
||||||
valueEncoding: "json",
|
|
||||||
}).then((userPreferences) => {
|
|
||||||
loadState(userPreferences);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ const processAchievementFileDiff = async (
|
||||||
export class AchievementWatcherManager {
|
export class AchievementWatcherManager {
|
||||||
private static hasFinishedMergingWithRemote = false;
|
private static hasFinishedMergingWithRemote = false;
|
||||||
|
|
||||||
public static watchAchievements = () => {
|
public static watchAchievements() {
|
||||||
if (!this.hasFinishedMergingWithRemote) return;
|
if (!this.hasFinishedMergingWithRemote) return;
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
|
@ -149,12 +149,12 @@ export class AchievementWatcherManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
return watchAchievementsWithWine();
|
return watchAchievementsWithWine();
|
||||||
};
|
}
|
||||||
|
|
||||||
private static preProcessGameAchievementFiles = (
|
private static preProcessGameAchievementFiles(
|
||||||
game: Game,
|
game: Game,
|
||||||
gameAchievementFiles: AchievementFile[]
|
gameAchievementFiles: AchievementFile[]
|
||||||
) => {
|
) {
|
||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
const parsedAchievements = parseAchievementFile(
|
const parsedAchievements = parseAchievementFile(
|
||||||
|
@ -182,7 +182,7 @@ export class AchievementWatcherManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeAchievements(game, unlockedAchievements, false);
|
return mergeAchievements(game, unlockedAchievements, false);
|
||||||
};
|
}
|
||||||
|
|
||||||
private static preSearchAchievementsWindows = async () => {
|
private static preSearchAchievementsWindows = async () => {
|
||||||
const games = await gamesSublevel
|
const games = await gamesSublevel
|
||||||
|
@ -230,7 +230,7 @@ export class AchievementWatcherManager {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public static preSearchAchievements = async () => {
|
public static async preSearchAchievements() {
|
||||||
try {
|
try {
|
||||||
const newAchievementsCount =
|
const newAchievementsCount =
|
||||||
process.platform === "win32"
|
process.platform === "win32"
|
||||||
|
@ -256,5 +256,5 @@ export class AchievementWatcherManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasFinishedMergingWithRemote = true;
|
this.hasFinishedMergingWithRemote = true;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const getGameAchievementData = async (
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("Failed to get game achievements", err);
|
logger.error("Failed to get game achievements for", objectId, err);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,7 +59,7 @@ export const mergeAchievements = async (
|
||||||
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
||||||
|
|
||||||
const newAchievementsMap = new Map(
|
const newAchievementsMap = new Map(
|
||||||
achievements.reverse().map((achievement) => {
|
achievements.toReversed().map((achievement) => {
|
||||||
return [achievement.name.toUpperCase(), achievement];
|
return [achievement.name.toUpperCase(), achievement];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -87,7 +87,7 @@ export const mergeAchievements = async (
|
||||||
userPreferences?.achievementNotificationsEnabled
|
userPreferences?.achievementNotificationsEnabled
|
||||||
) {
|
) {
|
||||||
const achievementsInfo = newAchievements
|
const achievementsInfo = newAchievements
|
||||||
.sort((a, b) => {
|
.toSorted((a, b) => {
|
||||||
return a.unlockTime - b.unlockTime;
|
return a.unlockTime - b.unlockTime;
|
||||||
})
|
})
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Downloader } from "@shared";
|
import { Downloader, DownloadError } from "@shared";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||||
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
|
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
|
||||||
import { PythonRPC } from "../python-rpc";
|
import { PythonRPC } from "../python-rpc";
|
||||||
import {
|
import {
|
||||||
LibtorrentPayload,
|
LibtorrentPayload,
|
||||||
|
@ -15,6 +15,7 @@ import path from "path";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
import { TorBoxClient } from "./torbox";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static downloadingGameId: string | null = null;
|
private static downloadingGameId: string | null = null;
|
||||||
|
@ -25,17 +26,20 @@ export class DownloadManager {
|
||||||
) {
|
) {
|
||||||
PythonRPC.spawn(
|
PythonRPC.spawn(
|
||||||
download?.status === "active"
|
download?.status === "active"
|
||||||
? await this.getDownloadPayload(download).catch(() => undefined)
|
? await this.getDownloadPayload(download).catch((err) => {
|
||||||
|
logger.error("Error getting download payload", err);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
downloadsToSeed?.map((download) => ({
|
downloadsToSeed?.map((download) => ({
|
||||||
game_id: `${download.shop}-${download.objectId}`,
|
game_id: levelKeys.game(download.shop, download.objectId),
|
||||||
url: download.uri,
|
url: download.uri,
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (download) {
|
if (download) {
|
||||||
this.downloadingGameId = `${download.shop}-${download.objectId}`;
|
this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +110,7 @@ export class DownloadManager {
|
||||||
|
|
||||||
if (!download || !game) return;
|
if (!download || !game) return;
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
|
@ -230,7 +234,9 @@ export class DownloadManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
|
|
||||||
if (downloadKey === this.downloadingGameId) {
|
if (downloadKey === this.downloadingGameId) {
|
||||||
|
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,6 +266,8 @@ export class DownloadManager {
|
||||||
const token = await GofileApi.authorize();
|
const token = await GofileApi.authorize();
|
||||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||||
|
|
||||||
|
await GofileApi.checkDownloadUrl(downloadLink);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: downloadId,
|
game_id: downloadId,
|
||||||
|
@ -270,10 +278,11 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
case Downloader.PixelDrain: {
|
case Downloader.PixelDrain: {
|
||||||
const id = download.uri.split("/").pop();
|
const id = download.uri.split("/").pop();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: downloadId,
|
game_id: downloadId,
|
||||||
url: `https://pixeldrain.com/api/file/${id}?download`,
|
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -295,6 +304,16 @@ export class DownloadManager {
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case Downloader.Mediafire: {
|
||||||
|
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: "start",
|
||||||
|
game_id: downloadId,
|
||||||
|
url: downloadUrl,
|
||||||
|
save_path: download.downloadPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
case Downloader.Torrent:
|
case Downloader.Torrent:
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
|
@ -305,10 +324,7 @@ export class DownloadManager {
|
||||||
case Downloader.RealDebrid: {
|
case Downloader.RealDebrid: {
|
||||||
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
if (!downloadUrl)
|
if (!downloadUrl) throw new Error(DownloadError.NotCachedInRealDebrid);
|
||||||
throw new Error(
|
|
||||||
"This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available."
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
|
@ -317,6 +333,18 @@ export class DownloadManager {
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case Downloader.TorBox: {
|
||||||
|
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
||||||
|
|
||||||
|
if (!url) return;
|
||||||
|
return {
|
||||||
|
action: "start",
|
||||||
|
game_id: downloadId,
|
||||||
|
url,
|
||||||
|
save_path: download.downloadPath,
|
||||||
|
out: name,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,24 +6,23 @@ import type {
|
||||||
TorBoxAddTorrentRequest,
|
TorBoxAddTorrentRequest,
|
||||||
TorBoxRequestLinkRequest,
|
TorBoxRequestLinkRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
export class TorBoxClient {
|
export class TorBoxClient {
|
||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
private static readonly baseURL = "https://api.torbox.app/v1/api";
|
private static readonly baseURL = "https://api.torbox.app/v1/api";
|
||||||
public static apiToken: string;
|
private static apiToken: string;
|
||||||
|
|
||||||
static authorize(apiToken: string) {
|
static authorize(apiToken: string) {
|
||||||
|
this.apiToken = apiToken;
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: this.baseURL,
|
baseURL: this.baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${apiToken}`,
|
Authorization: `Bearer ${apiToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.apiToken = apiToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addMagnet(magnet: string) {
|
private static async addMagnet(magnet: string) {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("magnet", magnet);
|
form.append("magnet", magnet);
|
||||||
|
|
||||||
|
@ -32,6 +31,10 @@ export class TorBoxClient {
|
||||||
form
|
form
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.detail);
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,22 +58,16 @@ export class TorBoxClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async requestLink(id: number) {
|
static async requestLink(id: number) {
|
||||||
const searchParams = new URLSearchParams({});
|
const searchParams = new URLSearchParams({
|
||||||
|
token: this.apiToken,
|
||||||
searchParams.set("token", this.apiToken);
|
torrent_id: id.toString(),
|
||||||
searchParams.set("torrent_id", id.toString());
|
zip_link: "true",
|
||||||
searchParams.set("zip_link", "true");
|
});
|
||||||
|
|
||||||
const response = await this.instance.get<TorBoxRequestLinkRequest>(
|
const response = await this.instance.get<TorBoxRequestLinkRequest>(
|
||||||
"/torrents/requestdl?" + searchParams.toString()
|
"/torrents/requestdl?" + searchParams.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
logger.error(response.data.error);
|
|
||||||
logger.error(response.data.detail);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +78,7 @@ export class TorBoxClient {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getTorrentId(magnetUri: string) {
|
private static async getTorrentIdAndName(magnetUri: string) {
|
||||||
const userTorrents = await this.getAllTorrentsFromUser();
|
const userTorrents = await this.getAllTorrentsFromUser();
|
||||||
|
|
||||||
const { infoHash } = await parseTorrent(magnetUri);
|
const { infoHash } = await parseTorrent(magnetUri);
|
||||||
|
@ -89,9 +86,18 @@ export class TorBoxClient {
|
||||||
(userTorrent) => userTorrent.hash === infoHash
|
(userTorrent) => userTorrent.hash === infoHash
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userTorrent) return userTorrent.id;
|
if (userTorrent) return { id: userTorrent.id, name: userTorrent.name };
|
||||||
|
|
||||||
const torrent = await this.addMagnet(magnetUri);
|
const torrent = await this.addMagnet(magnetUri);
|
||||||
return torrent.torrent_id;
|
return { id: torrent.torrent_id, name: torrent.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getDownloadInfo(uri: string) {
|
||||||
|
const torrentData = await this.getTorrentIdAndName(uri);
|
||||||
|
const url = await this.requestLink(torrentData.id);
|
||||||
|
|
||||||
|
const name = torrentData.name ? `${torrentData.name}.zip` : undefined;
|
||||||
|
|
||||||
|
return { url, name };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,4 +60,12 @@ export class GofileApi {
|
||||||
|
|
||||||
throw new Error("Failed to get download link");
|
throw new Error("Failed to get download link");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async checkDownloadUrl(url: string) {
|
||||||
|
return axios.head(url, {
|
||||||
|
headers: {
|
||||||
|
Cookie: `accountToken=${this.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./gofile";
|
export * from "./gofile";
|
||||||
export * from "./qiwi";
|
export * from "./qiwi";
|
||||||
export * from "./datanodes";
|
export * from "./datanodes";
|
||||||
|
export * from "./mediafire";
|
||||||
|
|
54
src/main/services/hosters/mediafire.ts
Normal file
54
src/main/services/hosters/mediafire.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
export class MediafireApi {
|
||||||
|
private static readonly validMediafireIdentifierDL = /^[a-zA-Z0-9]+$/m;
|
||||||
|
private static readonly validMediafirePreDL =
|
||||||
|
/(?<=['"])(https?:)?(\/\/)?(www\.)?mediafire\.com\/(file|view|download)\/[^'"?]+\?dkey=[^'"]+(?=['"])/;
|
||||||
|
private static readonly validDynamicDL =
|
||||||
|
/(?<=['"])https?:\/\/download\d+\.mediafire\.com\/[^'"]+(?=['"])/;
|
||||||
|
private static readonly checkHTTP = /^https?:\/\//m;
|
||||||
|
|
||||||
|
public static async getDownloadUrl(mediafireUrl: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const processedUrl = this.processUrl(mediafireUrl);
|
||||||
|
const response = await fetch(processedUrl);
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch Mediafire page");
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
return this.extractDirectUrl(html);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to get download URL`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static processUrl(url: string): string {
|
||||||
|
let processed = url.replace("http://", "https://");
|
||||||
|
|
||||||
|
if (this.validMediafireIdentifierDL.test(processed)) {
|
||||||
|
processed = `https://mediafire.com/?${processed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.checkHTTP.test(processed)) {
|
||||||
|
processed = processed.startsWith("//")
|
||||||
|
? `https:${processed}`
|
||||||
|
: `https://${processed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractDirectUrl(html: string): string {
|
||||||
|
const preMatch = this.validMediafirePreDL.exec(html);
|
||||||
|
if (preMatch?.[0]) {
|
||||||
|
return preMatch[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dlMatch = this.validDynamicDL.exec(html);
|
||||||
|
if (dlMatch?.[0]) {
|
||||||
|
return dlMatch[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("No valid download links found");
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { WindowManager } from "./window-manager";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import { uploadGamesBatch } from "./library-sync";
|
import { uploadGamesBatch } from "./library-sync";
|
||||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||||
import { logger } from "./logger";
|
import { networkLogger as logger } from "./logger";
|
||||||
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import { appVersion } from "@main/constants";
|
import { appVersion } from "@main/constants";
|
||||||
|
@ -154,7 +154,8 @@ export class HydraApi {
|
||||||
(error) => {
|
(error) => {
|
||||||
logger.error(" ---- RESPONSE ERROR -----");
|
logger.error(" ---- RESPONSE ERROR -----");
|
||||||
const { config } = error;
|
const { config } = error;
|
||||||
const data = JSON.parse(config.data);
|
|
||||||
|
const data = JSON.parse(config.data ?? null);
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
config.method,
|
config.method,
|
||||||
|
@ -175,14 +176,22 @@ export class HydraApi {
|
||||||
error.response.status,
|
error.response.status,
|
||||||
error.response.data
|
error.response.data
|
||||||
);
|
);
|
||||||
} else if (error.request) {
|
|
||||||
const errorData = error.toJSON();
|
return Promise.reject(error as Error);
|
||||||
logger.error("Request error:", errorData.message);
|
|
||||||
} else {
|
|
||||||
logger.error("Error", error.message);
|
|
||||||
}
|
}
|
||||||
logger.error(" ----- END RESPONSE ERROR -------");
|
|
||||||
return Promise.reject(error);
|
if (error.request) {
|
||||||
|
const errorData = error.toJSON();
|
||||||
|
logger.error("Request error:", errorData.code, errorData.message);
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
`Request failed with ${errorData.code} ${errorData.message}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Error", error.message);
|
||||||
|
return Promise.reject(error as Error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./crypto";
|
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./steam";
|
export * from "./steam";
|
||||||
export * from "./steam-250";
|
export * from "./steam-250";
|
||||||
|
|
|
@ -35,7 +35,6 @@ export const mergeWithRemoteGames = async () => {
|
||||||
name: "getById",
|
name: "getById",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (steamGame) {
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
const iconUrl = steamGame?.clientIcon
|
||||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
@ -52,7 +51,6 @@ export const mergeWithRemoteGames = async () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const uploadGamesBatch = async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const gamesChunks = chunk(games, 200);
|
const gamesChunks = chunk(games, 50);
|
||||||
|
|
||||||
for (const chunk of gamesChunks) {
|
for (const chunk of gamesChunks) {
|
||||||
await HydraApi.post(
|
await HydraApi.post(
|
||||||
|
|
|
@ -6,8 +6,12 @@ log.transports.file.resolvePathFn = (
|
||||||
_: log.PathVariables,
|
_: log.PathVariables,
|
||||||
message?: log.LogMessage | undefined
|
message?: log.LogMessage | undefined
|
||||||
) => {
|
) => {
|
||||||
if (message?.scope === "python-instance") {
|
if (message?.scope === "python-rpc") {
|
||||||
return path.join(logsPath, "pythoninstance.txt");
|
return path.join(logsPath, "pythonrpc.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message?.scope === "network") {
|
||||||
|
return path.join(logsPath, "network.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message?.scope == "achievements") {
|
if (message?.scope == "achievements") {
|
||||||
|
@ -34,3 +38,4 @@ log.initialize();
|
||||||
export const pythonRpcLogger = log.scope("python-rpc");
|
export const pythonRpcLogger = log.scope("python-rpc");
|
||||||
export const logger = log.scope("main");
|
export const logger = log.scope("main");
|
||||||
export const achievementsLogger = log.scope("achievements");
|
export const achievementsLogger = log.scope("achievements");
|
||||||
|
export const networkLogger = log.scope("network");
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { sleep } from "@main/helpers";
|
||||||
import { DownloadManager } from "./download";
|
import { DownloadManager } from "./download";
|
||||||
import { watchProcesses } from "./process-watcher";
|
import { watchProcesses } from "./process-watcher";
|
||||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||||
|
import { UpdateManager } from "./update-manager";
|
||||||
|
|
||||||
export const startMainLoop = async () => {
|
export const startMainLoop = async () => {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
@ -11,6 +12,7 @@ export const startMainLoop = async () => {
|
||||||
DownloadManager.watchDownloads(),
|
DownloadManager.watchDownloads(),
|
||||||
AchievementWatcherManager.watchAchievements(),
|
AchievementWatcherManager.watchAchievements(),
|
||||||
DownloadManager.getSeedStatus(),
|
DownloadManager.getSeedStatus(),
|
||||||
|
UpdateManager.checkForUpdatePeriodically(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { achievementSoundPath } from "@main/constants";
|
||||||
import icon from "@resources/icon.png?asset";
|
import icon from "@resources/icon.png?asset";
|
||||||
import { NotificationOptions, toXmlString } from "./xml";
|
import { NotificationOptions, toXmlString } from "./xml";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
import { WindowManager } from "../window-manager";
|
||||||
import type { Game, UserPreferences } from "@types";
|
import type { Game, UserPreferences } from "@types";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
|
@ -96,7 +97,9 @@ export const publishCombinedNewAchievementNotification = async (
|
||||||
toastXml: toXmlString(options),
|
toastXml: toXmlString(options),
|
||||||
}).show();
|
}).show();
|
||||||
|
|
||||||
if (process.platform !== "linux") {
|
if (WindowManager.mainWindow) {
|
||||||
|
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
|
||||||
|
} else if (process.platform !== "linux") {
|
||||||
sound.play(achievementSoundPath);
|
sound.play(achievementSoundPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -143,7 +146,9 @@ export const publishNewAchievementNotification = async (info: {
|
||||||
toastXml: toXmlString(options),
|
toastXml: toXmlString(options),
|
||||||
}).show();
|
}).show();
|
||||||
|
|
||||||
if (process.platform !== "linux") {
|
if (WindowManager.mainWindow) {
|
||||||
|
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
|
||||||
|
} else if (process.platform !== "linux") {
|
||||||
sound.play(achievementSoundPath);
|
sound.play(achievementSoundPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,11 +21,18 @@ export const getSteamAppDetails = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
.get(
|
.get<SteamAppDetailsResponse>(
|
||||||
`http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
|
`http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data[objectId].success) return response.data[objectId].data;
|
if (response.data[objectId].success) {
|
||||||
|
const data = response.data[objectId].data;
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
objectId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
60
src/main/services/update-manager.ts
Normal file
60
src/main/services/update-manager.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import updater, { UpdateInfo } from "electron-updater";
|
||||||
|
import { logger, WindowManager } from "@main/services";
|
||||||
|
import { AppUpdaterEvent } from "@types";
|
||||||
|
import { app } from "electron";
|
||||||
|
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
||||||
|
|
||||||
|
const isAutoInstallAvailable =
|
||||||
|
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
|
||||||
|
|
||||||
|
const { autoUpdater } = updater;
|
||||||
|
const sendEventsForDebug = false;
|
||||||
|
|
||||||
|
export class UpdateManager {
|
||||||
|
private static hasNotified = false;
|
||||||
|
private static newVersion = "";
|
||||||
|
private static checkTick = 0;
|
||||||
|
|
||||||
|
private static mockValuesForDebug() {
|
||||||
|
this.sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
||||||
|
this.sendEvent({ type: "update-downloaded" });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static sendEvent(event: AppUpdaterEvent) {
|
||||||
|
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static checkForUpdates() {
|
||||||
|
autoUpdater
|
||||||
|
.once("update-available", (info: UpdateInfo) => {
|
||||||
|
this.sendEvent({ type: "update-available", info });
|
||||||
|
this.newVersion = info.version;
|
||||||
|
})
|
||||||
|
.once("update-downloaded", () => {
|
||||||
|
this.sendEvent({ type: "update-downloaded" });
|
||||||
|
|
||||||
|
if (!this.hasNotified) {
|
||||||
|
this.hasNotified = true;
|
||||||
|
publishNotificationUpdateReadyToInstall(this.newVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (app.isPackaged) {
|
||||||
|
autoUpdater.autoDownload = isAutoInstallAvailable;
|
||||||
|
autoUpdater.checkForUpdates().then((result) => {
|
||||||
|
logger.log(`Check for updates result: ${result}`);
|
||||||
|
});
|
||||||
|
} else if (sendEventsForDebug) {
|
||||||
|
this.mockValuesForDebug();
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAutoInstallAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static checkForUpdatePeriodically() {
|
||||||
|
if (this.checkTick % 2000 == 0) {
|
||||||
|
this.checkForUpdates();
|
||||||
|
}
|
||||||
|
this.checkTick++;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,6 @@ export const getUserData = async () => {
|
||||||
})
|
})
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
if (err instanceof UserNotLoggedInError) {
|
if (err instanceof UserNotLoggedInError) {
|
||||||
logger.info("User is not logged in", err);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.error("Failed to get logged user");
|
logger.error("Failed to get logged user");
|
||||||
|
@ -59,6 +58,7 @@ export const getUserData = async () => {
|
||||||
expiresAt: loggedUser.subscription.expiresAt,
|
expiresAt: loggedUser.subscription.expiresAt,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
featurebaseJwt: "",
|
||||||
} as UserDetails;
|
} as UserDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { slice, sortBy } from "lodash-es";
|
import { slice, sortBy } from "lodash-es";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import { AuthPage } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
|
import { isStaging } from "@main/constants";
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||||
|
@ -52,7 +53,7 @@ export class WindowManager {
|
||||||
minHeight: 540,
|
minHeight: 540,
|
||||||
backgroundColor: "#1c1c1c",
|
backgroundColor: "#1c1c1c",
|
||||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
...(process.platform === "linux" ? { icon } : {}),
|
icon,
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
titleBarOverlay: {
|
titleBarOverlay: {
|
||||||
symbolColor: "#DADBE1",
|
symbolColor: "#DADBE1",
|
||||||
|
@ -129,7 +130,8 @@ export class WindowManager {
|
||||||
this.mainWindow.removeMenu();
|
this.mainWindow.removeMenu();
|
||||||
|
|
||||||
this.mainWindow.on("ready-to-show", () => {
|
this.mainWindow.on("ready-to-show", () => {
|
||||||
if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools();
|
if (!app.isPackaged || isStaging)
|
||||||
|
WindowManager.mainWindow?.webContents.openDevTools();
|
||||||
WindowManager.mainWindow?.show();
|
WindowManager.mainWindow?.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -147,6 +149,11 @@ export class WindowManager {
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
WindowManager.mainWindow = null;
|
WindowManager.mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mainWindow.webContents.setWindowOpenHandler((handler) => {
|
||||||
|
shell.openExternal(handler.url);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
|
public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
|
||||||
|
|
|
@ -33,10 +33,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
ipcRenderer.invoke("pauseGameSeed", shop, objectId),
|
ipcRenderer.invoke("pauseGameSeed", shop, objectId),
|
||||||
resumeGameSeed: (shop: GameShop, objectId: string) =>
|
resumeGameSeed: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("resumeGameSeed", shop, objectId),
|
ipcRenderer.invoke("resumeGameSeed", shop, objectId),
|
||||||
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
onDownloadProgress: (cb: (value: DownloadProgress | null) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
value: DownloadProgress
|
value: DownloadProgress | null
|
||||||
) => cb(value);
|
) => cb(value);
|
||||||
ipcRenderer.on("on-download-progress", listener);
|
ipcRenderer.on("on-download-progress", listener);
|
||||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||||
|
@ -93,6 +93,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
||||||
authenticateRealDebrid: (apiToken: string) =>
|
authenticateRealDebrid: (apiToken: string) =>
|
||||||
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
||||||
|
authenticateTorBox: (apiToken: string) =>
|
||||||
|
ipcRenderer.invoke("authenticateTorBox", apiToken),
|
||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
putDownloadSource: (objectIds: string[]) =>
|
putDownloadSource: (objectIds: string[]) =>
|
||||||
|
@ -109,11 +111,16 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) =>
|
) =>
|
||||||
ipcRenderer.invoke("updateExecutablePath", shop, objectId, executablePath),
|
ipcRenderer.invoke("updateExecutablePath", shop, objectId, executablePath),
|
||||||
|
addGameToFavorites: (shop: GameShop, objectId: string) =>
|
||||||
|
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||||
|
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||||
|
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||||
updateLaunchOptions: (
|
updateLaunchOptions: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
launchOptions: string | null
|
launchOptions: string | null
|
||||||
) => ipcRenderer.invoke("updateLaunchOptions", shop, objectId, launchOptions),
|
) => ipcRenderer.invoke("updateLaunchOptions", shop, objectId, launchOptions),
|
||||||
|
|
||||||
selectGameWinePrefix: (
|
selectGameWinePrefix: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
|
@ -170,6 +177,12 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
return () =>
|
return () =>
|
||||||
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
||||||
},
|
},
|
||||||
|
onAchievementUnlocked: (cb: () => void) => {
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
|
ipcRenderer.on("on-achievement-unlocked", listener);
|
||||||
|
return () =>
|
||||||
|
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
||||||
|
},
|
||||||
|
|
||||||
/* Hardware */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) =>
|
getDiskFreeSpace: (path: string) =>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
||||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -85,7 +85,7 @@ export function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onDownloadProgress(
|
const unsubscribe = window.electron.onDownloadProgress(
|
||||||
(downloadProgress) => {
|
(downloadProgress) => {
|
||||||
if (downloadProgress.progress === 1) {
|
if (downloadProgress?.progress === 1) {
|
||||||
clearDownload();
|
clearDownload();
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
return;
|
return;
|
||||||
|
@ -245,6 +245,22 @@ export function App() {
|
||||||
loadAndApplyTheme();
|
loadAndApplyTheme();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const playAudio = useCallback(() => {
|
||||||
|
const audio = new Audio(achievementSound);
|
||||||
|
audio.volume = 0.2;
|
||||||
|
audio.play();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onAchievementUnlocked(() => {
|
||||||
|
playAudio();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [playAudio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
||||||
if (cssString) {
|
if (cssString) {
|
||||||
|
@ -252,9 +268,7 @@ export function App() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => unsubscribe();
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
|
@ -276,9 +290,11 @@ export function App() {
|
||||||
|
|
||||||
<Toast
|
<Toast
|
||||||
visible={toast.visible}
|
visible={toast.visible}
|
||||||
|
title={toast.title}
|
||||||
message={toast.message}
|
message={toast.message}
|
||||||
type={toast.type}
|
type={toast.type}
|
||||||
onClose={handleToastClose}
|
onClose={handleToastClose}
|
||||||
|
duration={toast.duration}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HydraCloudModal
|
<HydraCloudModal
|
||||||
|
|
BIN
src/renderer/src/assets/audio/achievement.wav
Normal file
BIN
src/renderer/src/assets/audio/achievement.wav
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/icons/torbox.webp
Normal file
BIN
src/renderer/src/assets/icons/torbox.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -7,5 +7,6 @@
|
||||||
border: solid 1px globals.$muted-color;
|
border: solid 1px globals.$muted-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,4 +21,14 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__version-button {
|
||||||
|
color: globals.$body-color;
|
||||||
|
border-bottom: solid 1px transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-bottom: solid 1px globals.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,6 @@ export function BottomPanel() {
|
||||||
|
|
||||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||||
|
|
||||||
const isGameDownloading = !!lastPacket;
|
|
||||||
|
|
||||||
const [version, setVersion] = useState("");
|
const [version, setVersion] = useState("");
|
||||||
const [sessionHash, setSessionHash] = useState<null | string>("");
|
const [sessionHash, setSessionHash] = useState<null | string>("");
|
||||||
|
|
||||||
|
@ -33,9 +31,11 @@ export function BottomPanel() {
|
||||||
}, [userDetails?.id]);
|
}, [userDetails?.id]);
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
if (isGameDownloading) {
|
const game = lastPacket
|
||||||
const game = library.find((game) => game.id === lastPacket?.gameId)!;
|
? library.find((game) => game.id === lastPacket?.gameId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (game) {
|
||||||
if (lastPacket?.isCheckingFiles)
|
if (lastPacket?.isCheckingFiles)
|
||||||
return t("checking_files", {
|
return t("checking_files", {
|
||||||
title: game.title,
|
title: game.title,
|
||||||
|
@ -64,7 +64,7 @@ export function BottomPanel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return t("no_downloads_in_progress");
|
return t("no_downloads_in_progress");
|
||||||
}, [t, isGameDownloading, library, lastPacket, progress, eta, downloadSpeed]);
|
}, [t, library, lastPacket, progress, eta, downloadSpeed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bottom-panel">
|
<footer className="bottom-panel">
|
||||||
|
@ -76,10 +76,15 @@ export function BottomPanel() {
|
||||||
<small>{status}</small>
|
<small>{status}</small>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<small>
|
<button
|
||||||
|
data-featurebase-changelog
|
||||||
|
className="bottom-panel__version-button"
|
||||||
|
>
|
||||||
|
<small data-featurebase-changelog>
|
||||||
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
||||||
{VERSION_CODENAME}"
|
{VERSION_CODENAME}"
|
||||||
</small>
|
</small>
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
&__checkbox {
|
&__checkbox {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$dark-background-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -45,6 +47,9 @@
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:has(+ input:disabled) {
|
&:has(+ input:disabled) {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
50
src/renderer/src/components/sidebar/sidebar-game-item.tsx
Normal file
50
src/renderer/src/components/sidebar/sidebar-game-item.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
import { LibraryGame } from "@types";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
interface SidebarGameItemProps {
|
||||||
|
game: LibraryGame;
|
||||||
|
handleSidebarGameClick: (event: React.MouseEvent, game: LibraryGame) => void;
|
||||||
|
getGameTitle: (game: LibraryGame) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarGameItem({
|
||||||
|
game,
|
||||||
|
handleSidebarGameClick,
|
||||||
|
getGameTitle,
|
||||||
|
}: Readonly<SidebarGameItemProps>) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={game.id}
|
||||||
|
className={cn("sidebar__menu-item", {
|
||||||
|
"sidebar__menu-item--active":
|
||||||
|
location.pathname === `/game/${game.shop}/${game.objectId}`,
|
||||||
|
"sidebar__menu-item--muted": game.download?.status === "removed",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sidebar__menu-item-button"
|
||||||
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
|
>
|
||||||
|
{game.iconUrl ? (
|
||||||
|
<img
|
||||||
|
className="sidebar__game-icon"
|
||||||
|
src={game.iconUrl}
|
||||||
|
alt={game.title}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SteamLogo className="sidebar__game-icon" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="sidebar__menu-item-button-label">
|
||||||
|
{getGameTitle(game)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,11 +18,11 @@ import "./sidebar.scss";
|
||||||
|
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||||
|
import { SidebarGameItem } from "./sidebar-game-item";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
|
@ -206,6 +206,23 @@ export function Sidebar() {
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="sidebar__section">
|
||||||
|
<small className="sidebar__section-title">{t("favorites")}</small>
|
||||||
|
|
||||||
|
<ul className="sidebar__menu">
|
||||||
|
{sortedLibrary
|
||||||
|
.filter((game) => game.favorite)
|
||||||
|
.map((game) => (
|
||||||
|
<SidebarGameItem
|
||||||
|
key={game.id}
|
||||||
|
game={game}
|
||||||
|
handleSidebarGameClick={handleSidebarGameClick}
|
||||||
|
getGameTitle={getGameTitle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="sidebar__section">
|
<section className="sidebar__section">
|
||||||
<small className="sidebar__section-title">{t("my_library")}</small>
|
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||||
|
|
||||||
|
@ -217,38 +234,15 @@ export function Sidebar() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ul className="sidebar__menu">
|
<ul className="sidebar__menu">
|
||||||
{filteredLibrary.map((game) => (
|
{filteredLibrary
|
||||||
<li
|
.filter((game) => !game.favorite)
|
||||||
key={`${game.shop}-${game.objectId}`}
|
.map((game) => (
|
||||||
className={cn("sidebar__menu-item", {
|
<SidebarGameItem
|
||||||
"sidebar__menu-item--active":
|
key={game.id}
|
||||||
location.pathname ===
|
game={game}
|
||||||
`/game/${game.shop}/${game.objectId}`,
|
handleSidebarGameClick={handleSidebarGameClick}
|
||||||
"sidebar__menu-item--muted":
|
getGameTitle={getGameTitle}
|
||||||
game.download?.status === "removed",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sidebar__menu-item-button"
|
|
||||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
|
||||||
>
|
|
||||||
{game.iconUrl ? (
|
|
||||||
<img
|
|
||||||
className="sidebar__game-icon"
|
|
||||||
src={game.iconUrl}
|
|
||||||
alt={game.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<SteamLogo className="sidebar__game-icon" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="sidebar__menu-item-button-label">
|
|
||||||
{getGameTitle(game)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -55,11 +55,9 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
}, [props.type, isPasswordVisible]);
|
}, [props.type, isPasswordVisible]);
|
||||||
|
|
||||||
const hintContent = useMemo(() => {
|
const hintContent = useMemo(() => {
|
||||||
if (error && typeof error === "object" && "message" in error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
<small className="text-field-container__error-label">
|
<small className="text-field-container__error-label">{error}</small>
|
||||||
{error.message as string}
|
|
||||||
</small>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hint) return <small>{hint}</small>;
|
if (hint) return <small>{hint}</small>;
|
||||||
|
|
|
@ -1,30 +1,26 @@
|
||||||
@use "../../scss/globals.scss";
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
$toast-height: 80px;
|
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
animation-duration: 0.2s;
|
animation-duration: 0.2s;
|
||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: ease-in-out;
|
||||||
max-height: $toast-height;
|
position: absolute;
|
||||||
position: fixed;
|
background-color: globals.$dark-background-color;
|
||||||
background-color: globals.$background-color;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px globals.$border-color;
|
||||||
right: calc(globals.$spacing-unit * 2);
|
right: 16px;
|
||||||
//bottom panel height + 16px
|
bottom: 26px + globals.$spacing-unit;
|
||||||
bottom: calc(26px + #{globals.$spacing-unit * 2});
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
z-index: globals.$toast-z-index;
|
z-index: globals.$toast-z-index;
|
||||||
max-width: 500px;
|
max-width: 420px;
|
||||||
animation-name: slide-in;
|
animation-name: enter;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
||||||
&--closing {
|
&--closing {
|
||||||
animation-name: slide-out;
|
animation-name: exit;
|
||||||
transform: translateY(calc($toast-height + #{globals.$spacing-unit * 2}));
|
transform: translateY(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
|
@ -38,6 +34,7 @@ $toast-height: 80px;
|
||||||
&__message-container {
|
&__message-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: globals.$spacing-unit;
|
gap: globals.$spacing-unit;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__message {
|
&__message {
|
||||||
|
@ -62,37 +59,38 @@ $toast-height: 80px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
transition: color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__success-icon {
|
&__icon {
|
||||||
|
&--success {
|
||||||
color: globals.$success-color;
|
color: globals.$success-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__error-icon {
|
&--error {
|
||||||
color: globals.$danger-color;
|
color: globals.$danger-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__warning-icon {
|
&--warning {
|
||||||
color: globals.$warning-color;
|
color: globals.$warning-color;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in {
|
@keyframes enter {
|
||||||
0% {
|
0% {
|
||||||
transform: translateY(calc($toast-height + #{globals.$spacing-unit * 2}));
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
100% {
|
@keyframes exit {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-out {
|
|
||||||
0% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translateY(calc($toast-height + #{globals.$spacing-unit * 2}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,14 +11,23 @@ import cn from "classnames";
|
||||||
|
|
||||||
export interface ToastProps {
|
export interface ToastProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
message: string;
|
title: string;
|
||||||
|
message?: string;
|
||||||
type: "success" | "error" | "warning";
|
type: "success" | "error" | "warning";
|
||||||
|
duration?: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_PROGRESS = 100;
|
const INITIAL_PROGRESS = 100;
|
||||||
|
|
||||||
export function Toast({ visible, message, type, onClose }: ToastProps) {
|
export function Toast({
|
||||||
|
visible,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
duration = 2500,
|
||||||
|
onClose,
|
||||||
|
}: Readonly<ToastProps>) {
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const [progress, setProgress] = useState(INITIAL_PROGRESS);
|
const [progress, setProgress] = useState(INITIAL_PROGRESS);
|
||||||
|
|
||||||
|
@ -31,7 +40,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||||
|
|
||||||
closingAnimation.current = requestAnimationFrame(
|
closingAnimation.current = requestAnimationFrame(
|
||||||
function animateClosing(time) {
|
function animateClosing(time) {
|
||||||
if (time - zero <= 200) {
|
if (time - zero <= 150) {
|
||||||
closingAnimation.current = requestAnimationFrame(animateClosing);
|
closingAnimation.current = requestAnimationFrame(animateClosing);
|
||||||
} else {
|
} else {
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -43,17 +52,13 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
const zero = performance.now();
|
const zero = performance.now();
|
||||||
|
|
||||||
progressAnimation.current = requestAnimationFrame(
|
progressAnimation.current = requestAnimationFrame(
|
||||||
function animateProgress(time) {
|
function animateProgress(time) {
|
||||||
const elapsed = time - zero;
|
const elapsed = time - zero;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
const progress = Math.min(elapsed / 2500, 1);
|
|
||||||
const currentValue =
|
const currentValue =
|
||||||
INITIAL_PROGRESS + (0 - INITIAL_PROGRESS) * progress;
|
INITIAL_PROGRESS + (0 - INITIAL_PROGRESS) * progress;
|
||||||
|
|
||||||
setProgress(currentValue);
|
setProgress(currentValue);
|
||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
progressAnimation.current = requestAnimationFrame(animateProgress);
|
progressAnimation.current = requestAnimationFrame(animateProgress);
|
||||||
} else {
|
} else {
|
||||||
|
@ -70,9 +75,8 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||||
setIsClosing(false);
|
setIsClosing(false);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [startAnimateClosing, visible]);
|
}, [startAnimateClosing, duration, visible]);
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
|
@ -84,17 +88,27 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||||
>
|
>
|
||||||
<div className="toast__content">
|
<div className="toast__content">
|
||||||
<div className="toast__message-container">
|
<div className="toast__message-container">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `8px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{type === "success" && (
|
{type === "success" && (
|
||||||
<CheckCircleFillIcon className="toast__success-icon" />
|
<CheckCircleFillIcon className="toast__icon--success" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "error" && (
|
{type === "error" && (
|
||||||
<XCircleFillIcon className="toast__error-icon" />
|
<XCircleFillIcon className="toast__icon--error" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "warning" && <AlertIcon className="toast__warning-icon" />}
|
{type === "warning" && (
|
||||||
<span className="toast__message">{message}</span>
|
<AlertIcon className="toast__icon--warning" />
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
<span style={{ fontWeight: "bold", flex: 1 }}>{title}</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -106,6 +120,10 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{message && <p>{message}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<progress className="toast__progress" value={progress} max={100} />
|
<progress className="toast__progress" value={progress} max={100} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const DOWNLOADER_NAME = {
|
||||||
[Downloader.PixelDrain]: "PixelDrain",
|
[Downloader.PixelDrain]: "PixelDrain",
|
||||||
[Downloader.Qiwi]: "Qiwi",
|
[Downloader.Qiwi]: "Qiwi",
|
||||||
[Downloader.Datanodes]: "Datanodes",
|
[Downloader.Datanodes]: "Datanodes",
|
||||||
|
[Downloader.Mediafire]: "Mediafire",
|
||||||
|
[Downloader.TorBox]: "TorBox",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
14
src/renderer/src/declaration.d.ts
vendored
14
src/renderer/src/declaration.d.ts
vendored
|
@ -28,6 +28,7 @@ import type {
|
||||||
CatalogueSearchPayload,
|
CatalogueSearchPayload,
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
|
TorBoxUser,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
|
@ -40,14 +41,16 @@ declare global {
|
||||||
|
|
||||||
interface Electron {
|
interface Electron {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
startGameDownload: (payload: StartGameDownloadPayload) => Promise<void>;
|
startGameDownload: (
|
||||||
|
payload: StartGameDownloadPayload
|
||||||
|
) => Promise<{ ok: boolean; error?: string }>;
|
||||||
cancelGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
cancelGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
pauseGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
pauseGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
resumeGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
resumeGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
pauseGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
pauseGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
resumeGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
resumeGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
onDownloadProgress: (
|
onDownloadProgress: (
|
||||||
cb: (value: DownloadProgress) => void
|
cb: (value: DownloadProgress | null) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
onSeedingStatus: (
|
onSeedingStatus: (
|
||||||
cb: (value: SeedingStatus[]) => void
|
cb: (value: SeedingStatus[]) => void
|
||||||
|
@ -93,6 +96,11 @@ declare global {
|
||||||
objectId: string,
|
objectId: string,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
addGameToFavorites: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
|
removeGameFromFavorites: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => Promise<void>;
|
||||||
updateLaunchOptions: (
|
updateLaunchOptions: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
|
@ -142,6 +150,8 @@ declare global {
|
||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
|
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||||
|
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
putDownloadSource: (
|
putDownloadSource: (
|
||||||
|
|
|
@ -18,9 +18,9 @@ export const downloadSlice = createSlice({
|
||||||
name: "download",
|
name: "download",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setLastPacket: (state, action: PayloadAction<DownloadProgress>) => {
|
setLastPacket: (state, action: PayloadAction<DownloadProgress | null>) => {
|
||||||
state.lastPacket = action.payload;
|
state.lastPacket = action.payload;
|
||||||
if (!state.gameId) state.gameId = action.payload.gameId;
|
if (!state.gameId && action.payload) state.gameId = action.payload.gameId;
|
||||||
},
|
},
|
||||||
clearDownload: (state) => {
|
clearDownload: (state) => {
|
||||||
state.lastPacket = null;
|
state.lastPacket = null;
|
||||||
|
|
|
@ -3,14 +3,18 @@ import type { PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { ToastProps } from "@renderer/components/toast/toast";
|
import { ToastProps } from "@renderer/components/toast/toast";
|
||||||
|
|
||||||
export interface ToastState {
|
export interface ToastState {
|
||||||
message: string;
|
title: string;
|
||||||
|
message?: string;
|
||||||
type: ToastProps["type"];
|
type: ToastProps["type"];
|
||||||
|
duration?: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ToastState = {
|
const initialState: ToastState = {
|
||||||
|
title: "",
|
||||||
message: "",
|
message: "",
|
||||||
type: "success",
|
type: "success",
|
||||||
|
duration: 5000,
|
||||||
visible: false,
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,8 +23,10 @@ export const toastSlice = createSlice({
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
|
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
|
||||||
|
state.title = action.payload.title;
|
||||||
state.message = action.payload.message;
|
state.message = action.payload.message;
|
||||||
state.type = action.payload.type;
|
state.type = action.payload.type;
|
||||||
|
state.duration = action.payload.duration ?? 2000;
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
},
|
},
|
||||||
closeToast: (state) => {
|
closeToast: (state) => {
|
||||||
|
|
|
@ -29,10 +29,11 @@ export function useDownload() {
|
||||||
const startDownload = async (payload: StartGameDownloadPayload) => {
|
const startDownload = async (payload: StartGameDownloadPayload) => {
|
||||||
dispatch(clearDownload());
|
dispatch(clearDownload());
|
||||||
|
|
||||||
const game = await window.electron.startGameDownload(payload);
|
const response = await window.electron.startGameDownload(payload);
|
||||||
|
|
||||||
await updateLibrary();
|
if (response.ok) updateLibrary();
|
||||||
return game;
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
||||||
|
@ -113,7 +114,7 @@ export function useDownload() {
|
||||||
pauseSeeding,
|
pauseSeeding,
|
||||||
resumeSeeding,
|
resumeSeeding,
|
||||||
clearDownload: () => dispatch(clearDownload()),
|
clearDownload: () => dispatch(clearDownload()),
|
||||||
setLastPacket: (packet: DownloadProgress) =>
|
setLastPacket: (packet: DownloadProgress | null) =>
|
||||||
dispatch(setLastPacket(packet)),
|
dispatch(setLastPacket(packet)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,13 @@ export function useToast() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const showSuccessToast = useCallback(
|
const showSuccessToast = useCallback(
|
||||||
(message: string) => {
|
(title: string, message?: string, duration?: number) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
|
title,
|
||||||
message,
|
message,
|
||||||
type: "success",
|
type: "success",
|
||||||
|
duration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -18,11 +20,13 @@ export function useToast() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const showErrorToast = useCallback(
|
const showErrorToast = useCallback(
|
||||||
(message: string) => {
|
(title: string, message?: string, duration?: number) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
|
title,
|
||||||
message,
|
message,
|
||||||
type: "error",
|
type: "error",
|
||||||
|
duration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -30,11 +34,13 @@ export function useToast() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const showWarningToast = useCallback(
|
const showWarningToast = useCallback(
|
||||||
(message: string) => {
|
(title: string, message?: string, duration?: number) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
|
title,
|
||||||
message,
|
message,
|
||||||
type: "warning",
|
type: "warning",
|
||||||
|
duration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -78,9 +78,15 @@ export function useUserDetails() {
|
||||||
...response,
|
...response,
|
||||||
username: userDetails?.username || "",
|
username: userDetails?.username || "",
|
||||||
subscription: userDetails?.subscription || null,
|
subscription: userDetails?.subscription || null,
|
||||||
|
featurebaseJwt: userDetails?.featurebaseJwt || "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[updateUserDetails, userDetails?.username, userDetails?.subscription]
|
[
|
||||||
|
updateUserDetails,
|
||||||
|
userDetails?.username,
|
||||||
|
userDetails?.subscription,
|
||||||
|
userDetails?.featurebaseJwt,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncFriendRequests = useCallback(async () => {
|
const syncFriendRequests = useCallback(async () => {
|
||||||
|
|
|
@ -48,6 +48,7 @@ Sentry.init({
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
replaysSessionSampleRate: 0.1,
|
replaysSessionSampleRate: 0.1,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
release: await window.electron.getVersion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log = logger.log;
|
console.log = logger.log;
|
||||||
|
|
|
@ -10,7 +10,9 @@ interface AchievementListProps {
|
||||||
achievements: UserAchievement[];
|
achievements: UserAchievement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AchievementList({ achievements }: AchievementListProps) {
|
export function AchievementList({
|
||||||
|
achievements,
|
||||||
|
}: Readonly<AchievementListProps>) {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
const { showHydraCloudModal } = useSubscription();
|
const { showHydraCloudModal } = useSubscription();
|
||||||
const { formatDateTime } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
.achievement-panel {
|
.achievement-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: globals.$spacing-unit * 2 globals.$spacing-unit * 3;
|
padding: globals.$spacing-unit * 2 globals.$spacing-unit * 3;
|
||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$background-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
|
@ -66,9 +66,9 @@ $logo-max-width: 200px;
|
||||||
|
|
||||||
&__table-header {
|
&__table-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--color-dark-background);
|
background-color: globals.$dark-background-color;
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
border-bottom: solid 1px var(--color-border);
|
border-bottom: solid 1px globals.$border-color;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -86,13 +86,13 @@ $logo-max-width: 200px;
|
||||||
gap: globals.$spacing-unit * 2;
|
gap: globals.$spacing-unit * 2;
|
||||||
padding: globals.$spacing-unit * 2;
|
padding: globals.$spacing-unit * 2;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--color-background);
|
background-color: globals.$background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: all ease 0.1s;
|
transition: all ease 0.1s;
|
||||||
color: var(--color-muted);
|
color: globals.$muted-color;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -102,7 +102,7 @@ $logo-max-width: 200px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
background-color: globals.$border-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ $logo-max-width: 200px;
|
||||||
|
|
||||||
&-hidden-icon {
|
&-hidden-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--color-warning);
|
color: globals.$warning-color;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -164,7 +164,7 @@ $logo-max-width: 200px;
|
||||||
|
|
||||||
&--locked {
|
&--locked {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-warning);
|
color: globals.$warning-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-icon {
|
&-icon {
|
||||||
|
@ -219,12 +219,12 @@ $logo-max-width: 200px;
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
&::-webkit-progress-bar {
|
&::-webkit-progress-bar {
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
background-color: globals.$border-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-progress-value {
|
&::-webkit-progress-value {
|
||||||
background-color: var(--color-muted);
|
background-color: globals.$muted-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ $logo-max-width: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-background);
|
background-color: globals.$background-color;
|
||||||
position: relative;
|
position: relative;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ $logo-max-width: 200px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: math.div(globals.$spacing-unit, 2);
|
gap: math.div(globals.$spacing-unit, 2);
|
||||||
color: var(--color-body);
|
color: globals.$muted-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
||||||
|
|
||||||
export interface DownloadGroupProps {
|
export interface DownloadGroupProps {
|
||||||
library: LibraryGame[];
|
library: LibraryGame[];
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -235,12 +237,16 @@ export function DownloadGroup({
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isResumeDisabled =
|
||||||
|
(download?.downloader === Downloader.RealDebrid &&
|
||||||
|
!userPreferences?.realDebridApiToken) ||
|
||||||
|
(download?.downloader === Downloader.TorBox &&
|
||||||
|
!userPreferences?.torBoxApiToken);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: t("resume"),
|
label: t("resume"),
|
||||||
disabled:
|
disabled: isResumeDisabled,
|
||||||
download?.downloader === Downloader.RealDebrid &&
|
|
||||||
!userPreferences?.realDebridApiToken,
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
resumeDownload(game.shop, game.objectId);
|
resumeDownload(game.shop, game.objectId);
|
||||||
},
|
},
|
||||||
|
@ -279,13 +285,20 @@ export function DownloadGroup({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="download-group__cover-content">
|
<div className="download-group__cover-content">
|
||||||
|
{game.download?.downloader === Downloader.TorBox ? (
|
||||||
<Badge>
|
<Badge>
|
||||||
{
|
<img
|
||||||
DOWNLOADER_NAME[
|
src={torBoxLogo}
|
||||||
game?.download?.downloader as Downloader
|
alt="TorBox"
|
||||||
]
|
style={{ width: 13 }}
|
||||||
}
|
/>
|
||||||
|
<span>TorBox</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge>
|
||||||
|
{DOWNLOADER_NAME[game.download!.downloader]}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import "./downloads.scss";
|
||||||
import { DeleteGameModal } from "./delete-game-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { DownloadGroup } from "./download-group";
|
import { DownloadGroup } from "./download-group";
|
||||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy, sortBy } from "lodash-es";
|
||||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export default function Downloads() {
|
export default function Downloads() {
|
||||||
|
@ -58,7 +58,8 @@ export default function Downloads() {
|
||||||
complete: [],
|
complete: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = library.reduce((prev, next) => {
|
const result = sortBy(library, (game) => game.download?.timestamp).reduce(
|
||||||
|
(prev, next) => {
|
||||||
/* Game has been manually added to the library or has been canceled */
|
/* Game has been manually added to the library or has been canceled */
|
||||||
if (!next.download?.status || next.download?.status === "removed")
|
if (!next.download?.status || next.download?.status === "removed")
|
||||||
return prev;
|
return prev;
|
||||||
|
@ -72,7 +73,9 @@ export default function Downloads() {
|
||||||
return { ...prev, queued: [...prev.queued, next] };
|
return { ...prev, queued: [...prev.queued, next] };
|
||||||
|
|
||||||
return { ...prev, complete: [...prev.complete, next] };
|
return { ...prev, complete: [...prev.complete, next] };
|
||||||
}, initialValue);
|
},
|
||||||
|
initialValue
|
||||||
|
);
|
||||||
|
|
||||||
const queued = orderBy(result.queued, (game) => game.download?.timestamp, [
|
const queued = orderBy(result.queued, (game) => game.download?.timestamp, [
|
||||||
"desc",
|
"desc",
|
||||||
|
|
|
@ -189,14 +189,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{uploadingBackup && (
|
|
||||||
<progress
|
|
||||||
className="cloud-sync-modal__progress"
|
|
||||||
value={backupDownloadProgress?.progress ?? 0}
|
|
||||||
max={100}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="cloud-sync-modal__backups-header">
|
<div className="cloud-sync-modal__backups-header">
|
||||||
<h2>{t("backups")}</h2>
|
<h2>{t("backups")}</h2>
|
||||||
<small>
|
<small>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue