mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge branch 'main' into feat/macos-test
# Conflicts: # .github/workflows/build.yml
This commit is contained in:
commit
8705efc9b6
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@ -22,24 +22,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Install Python
|
||||
if: matrix.os == 'macos-11'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9.0
|
||||
|
||||
- name: Install Python
|
||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'windows-latest'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
- name: Build with cx_Freeze
|
||||
run: python torrent-client/setup.py build
|
||||
|
||||
- name: Build Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: yarn build:linux
|
||||
|
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@ -24,17 +24,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
- name: Build with cx_Freeze
|
||||
run: python torrent-client/setup.py build
|
||||
|
||||
- name: Build Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: yarn build:linux
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
.vscode
|
||||
node_modules
|
||||
hydra-download-manager
|
||||
aria2/
|
||||
fastlist.exe
|
||||
__pycache__
|
||||
dist
|
||||
|
@ -3,10 +3,10 @@ productName: Hydra
|
||||
directories:
|
||||
buildResources: build
|
||||
extraResources:
|
||||
- hydra-download-manager
|
||||
- aria2
|
||||
- seeds
|
||||
- hydra.db
|
||||
- fastlist.exe
|
||||
- seeds
|
||||
files:
|
||||
- "!**/.vscode/*"
|
||||
- "!src/*"
|
||||
|
@ -4,7 +4,6 @@
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
"homepage": "https://hydralauncher.site",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hydralauncher/hydra.git"
|
||||
@ -41,6 +40,7 @@
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
"@vanilla-extract/css": "^1.14.2",
|
||||
"@vanilla-extract/recipes": "^0.5.2",
|
||||
"aria2": "^4.1.2",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.6.8",
|
||||
"better-sqlite3": "^9.5.0",
|
||||
@ -49,7 +49,6 @@
|
||||
"color": "^4.2.3",
|
||||
"color.js": "^1.2.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"easydl": "^1.1.1",
|
||||
"electron-log": "^5.1.4",
|
||||
"electron-updater": "^6.1.8",
|
||||
"fetch-cookie": "^3.0.1",
|
||||
@ -59,7 +58,6 @@
|
||||
"jsdom": "^24.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lottie-react": "^2.4.0",
|
||||
"node-7z-archive": "^1.1.7",
|
||||
"parse-torrent": "^11.0.16",
|
||||
"ps-list": "^8.1.1",
|
||||
"react-i18next": "^14.1.0",
|
||||
|
@ -1,4 +1,51 @@
|
||||
const fs = require("fs");
|
||||
const { default: axios } = require("axios");
|
||||
const util = require("node:util");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const exec = util.promisify(require("node:child_process").exec);
|
||||
|
||||
const downloadAria2 = async () => {
|
||||
if (fs.existsSync("aria2")) {
|
||||
console.log("Aria2 already exists, skipping download...");
|
||||
return;
|
||||
}
|
||||
|
||||
const file =
|
||||
process.platform === "win32"
|
||||
? "aria2-1.37.0-win-64bit-build1.zip"
|
||||
: "aria2-1.37.0-1-x86_64.pkg.tar.zst";
|
||||
|
||||
const downloadUrl =
|
||||
process.platform === "win32"
|
||||
? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
|
||||
: "https://archlinux.org/packages/extra/x86_64/aria2/download/";
|
||||
|
||||
console.log(`Downloading ${file}...`);
|
||||
|
||||
const response = await axios.get(downloadUrl, { responseType: "stream" });
|
||||
|
||||
const stream = response.data.pipe(fs.createWriteStream(file));
|
||||
|
||||
stream.on("finish", async () => {
|
||||
console.log(`Downloaded ${file}, extracting...`);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
await exec(`npx extract-zip ${file}`);
|
||||
console.log("Extracted. Renaming folder...");
|
||||
|
||||
fs.renameSync(file.replace(".zip", ""), "aria2");
|
||||
} else {
|
||||
await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
|
||||
console.log("Extracted. Copying binary file...");
|
||||
fs.mkdirSync("aria2");
|
||||
fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
|
||||
fs.rmSync("usr", { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Extracted ${file}, removing compressed downloaded file...`);
|
||||
fs.rmSync(file);
|
||||
});
|
||||
};
|
||||
|
||||
if (process.platform === "win32") {
|
||||
fs.copyFileSync(
|
||||
@ -6,3 +53,5 @@ if (process.platform === "win32") {
|
||||
"fastlist.exe"
|
||||
);
|
||||
}
|
||||
|
||||
downloadAria2();
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "إعدادات",
|
||||
"my_library": "مكتبتي",
|
||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||
"checking_files": "{{title}} ({{percentage}} - جارٍ التحقق من الملفات...)",
|
||||
"paused": "{{title}} (متوقف)",
|
||||
"downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)",
|
||||
"filter": "بحث في المكتبة",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "لا يوجد تنزيلات جارية",
|
||||
"downloading_metadata": "جارٍ تنزيل بيانات وصف {{title}}",
|
||||
"checking_files": "جارٍ التحقق من ملفات {{title}}… ({{percentage}} مكتملة)",
|
||||
"downloading": "جارٍ تنزيل {{title}}… ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} متبقية على القرص",
|
||||
"eta": "الوقت المتبقي {{eta}}",
|
||||
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||
"checking_files": "جاري التحقق من الملفات...",
|
||||
"filter": "تصفية حزم إعادة التجميع",
|
||||
"requirements": "متطلبات النظام",
|
||||
"minimum": "الحد الأدنى",
|
||||
"recommended": "موصى به",
|
||||
"no_minimum_requirements": "{{title}} لا تتوفر معلومات عن الحد الأدنى للمتطلبات",
|
||||
"no_recommended_requirements": "{{title}} لا تتوفر معلومات عن المتطلبات الموصى بها",
|
||||
"paused_progress": "{{progress}} (متوقف)",
|
||||
"release_date": "تم الإصدار في {{date}}",
|
||||
"publisher": "نشر بواسطة {{publisher}}",
|
||||
"copy_link_to_clipboard": "نسخ الرابط",
|
||||
@ -120,22 +116,18 @@
|
||||
"verifying": "جار التحقق…",
|
||||
"completed_at": "اكتمل في {{date}}",
|
||||
"completed": "اكتمل",
|
||||
"cancelled": "ملغي",
|
||||
"download_again": "تحميل مرة أخرى",
|
||||
"cancel": "إلغاء",
|
||||
"filter": "تصفية الألعاب التي تم تنزيلها",
|
||||
"remove": "إزالة",
|
||||
"downloading_metadata": "جار تنزيل البيانات الوصفية…",
|
||||
"checking_files": "جار التحقق من الملفات…",
|
||||
"starting_download": "يبدأ التنزيل…",
|
||||
"deleting": "جار حذف المثبت…",
|
||||
"delete": "إزالة المثبت",
|
||||
"remove_from_list": "إزالة",
|
||||
"delete_modal_title": "هل أنت متأكد؟",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
|
||||
"install": "تثبيت",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "تورنت"
|
||||
"install": "تثبيت"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "مسار التنزيلات",
|
||||
@ -145,14 +137,13 @@
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة جديدة",
|
||||
"telemetry": "القياس عن بعد",
|
||||
"telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر",
|
||||
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal Debrid ",
|
||||
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal-Debrid ",
|
||||
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
|
||||
"launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام",
|
||||
"general": "عام",
|
||||
"behavior": "السلوك",
|
||||
"enable_real_debrid": "تفعيل Real Debrid ",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا.",
|
||||
"enable_real_debrid": "تفعيل Real-Debrid ",
|
||||
"real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا",
|
||||
"save_changes": "حفظ التغييرات"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Налады",
|
||||
"my_library": "Мая бібліятэка",
|
||||
"downloading_metadata": "{{title}} (Сцягванне мэтаданых…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Праверка файлаў…)",
|
||||
"paused": "{{title}} (Спынена)",
|
||||
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
||||
"filter": "Фільтар бібліятэкі",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Няма актыўных сцягванняў",
|
||||
"downloading_metadata": "Сцягванне мэтаданых {{title}}…",
|
||||
"checking_files": "Праверка файлаў {{title}}… ({{percentage}} скончана)",
|
||||
"downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} засталося на дыску",
|
||||
"eta": "Канчатак {{eta}}",
|
||||
"downloading_metadata": "Сцягванне мэтаданых…",
|
||||
"checking_files": "Праверка файлаў…",
|
||||
"filter": "Фільтар рэпакаў",
|
||||
"requirements": "Сістэмныя патрэбаванни",
|
||||
"minimum": "Мінімальныя",
|
||||
"recommended": "Рэкамендуемыя",
|
||||
"no_minimum_requirements": "{{title}} ня ўтрымлівае інфармацыі пра мінімальныя патрабаванні",
|
||||
"no_recommended_requirements": "{{title}} ня ўтрымлівае інфармацыі пра рэкамендуемыя патрабаванні",
|
||||
"paused_progress": "{{progress}} (Спынена)",
|
||||
"release_date": "Выпушчана {{date}}",
|
||||
"publisher": "Выдана {{publisher}}",
|
||||
"copy_link_to_clipboard": "Скапіяваць спасылку",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "Зараз гуляе",
|
||||
"change": "Змяніць",
|
||||
"repacks_modal_description": "Абярыце рэпак, які хочаце сцягнуць",
|
||||
"downloads_path": "Шлях сцягвання",
|
||||
"select_folder_hint": "Каб змяніць папку па змоўчанні, адкрыйце",
|
||||
"download_now": "Сцягнуць зараз",
|
||||
"installation_instructions": "Інструкцыя ўсталёўкі",
|
||||
@ -114,13 +109,11 @@
|
||||
"verifying": "Праверка…",
|
||||
"completed_at": "Скончана а {{date}}",
|
||||
"completed": "Скончана",
|
||||
"cancelled": "Скасавана",
|
||||
"download_again": "Сцягнуць зноў",
|
||||
"cancel": "Скасаваць",
|
||||
"filter": "Фільтар сцягнутых гульняў",
|
||||
"remove": "Выдаліць",
|
||||
"downloading_metadata": "Сцягванне мэтаданых…",
|
||||
"checking_files": "Праверка файлаў…",
|
||||
"starting_download": "Пачатак сцягвання…",
|
||||
"deleting": "Выдаленне ўсталёўшчыка…",
|
||||
"delete": "Выдаліць усталёўшчык",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Indstillinger",
|
||||
"my_library": "Mit bibliotek",
|
||||
"downloading_metadata": "{{title}} (Downloader metadata…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Tjekker filer…)",
|
||||
"paused": "{{title}} (Paused)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filtrer bibliotek",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Ingen downloads igang",
|
||||
"downloading_metadata": "Downloader {{title}} metadata…",
|
||||
"checking_files": "Tjekker {{title}} filer… ({{percentage}} færdig)",
|
||||
"downloading": "Downloader {{title}}… ({{percentage}} færdig) - Konklusion {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} tilbage på harddisken",
|
||||
"eta": "Konklusion {{eta}}",
|
||||
"downloading_metadata": "Downloader metadata…",
|
||||
"checking_files": "Tjekker filer…",
|
||||
"filter": "Filtrer repacks",
|
||||
"requirements": "System behov",
|
||||
"minimum": "Mindste",
|
||||
"recommended": "Anbefalet",
|
||||
"no_minimum_requirements": "{{title}} angiver ikke mindste behov informationer",
|
||||
"no_recommended_requirements": "{{title}} angiver ikke anbefalet behov informationer",
|
||||
"paused_progress": "{{progress}} (Pauset)",
|
||||
"release_date": "Offentliggjort den {{date}}",
|
||||
"publisher": "Udgivet af {{publisher}}",
|
||||
"copy_link_to_clipboard": "Kopier link",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "Spiller nu",
|
||||
"change": "Ændré",
|
||||
"repacks_modal_description": "Vælg den repack du vil downloade",
|
||||
"downloads_path": "Downloads sti",
|
||||
"select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne</0>",
|
||||
"download_now": "Download nu",
|
||||
"installation_instructions": "Installations Instrukser",
|
||||
@ -114,22 +109,18 @@
|
||||
"verifying": "Verificerer…",
|
||||
"completed_at": "Færdiggjort på {{date}}",
|
||||
"completed": "Færdigt",
|
||||
"cancelled": "Annulleret",
|
||||
"download_again": "Download igen",
|
||||
"cancel": "Annullér",
|
||||
"filter": "Filtrer downloadet spil",
|
||||
"remove": "Fjern",
|
||||
"downloading_metadata": "Downloader metadata…",
|
||||
"checking_files": "Tjekker filer…",
|
||||
"starting_download": "Starter download…",
|
||||
"deleting": "Sletter installatør…",
|
||||
"delete": "Fjern installatør",
|
||||
"remove_from_list": "Fjern",
|
||||
"delete_modal_title": "Er du sikker?",
|
||||
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
|
||||
"install": "Installér",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "Installér"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads sti",
|
||||
@ -139,14 +130,12 @@
|
||||
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
|
||||
"telemetry": "Telemetri",
|
||||
"telemetry_description": "Slå anonymt brugs statistik til",
|
||||
"real_debrid_api_token_description": "Real Debrid API token",
|
||||
"quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen",
|
||||
"launch_with_system": "Åben Hydra ved start af systemet",
|
||||
"general": "Generelt",
|
||||
"behavior": "Opførsel",
|
||||
"enable_real_debrid": "Slå Real Debrid til",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>.",
|
||||
"enable_real_debrid": "Slå Real-Debrid til",
|
||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
||||
"save_changes": "Gem ændringer"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Settings",
|
||||
"my_library": "My library",
|
||||
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Checking files…)",
|
||||
"paused": "{{title}} (Paused)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filter library",
|
||||
@ -35,8 +34,8 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "No downloads in progress",
|
||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)",
|
||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
|
||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Next page",
|
||||
@ -56,15 +55,15 @@
|
||||
"remove_from_list": "Remove",
|
||||
"space_left_on_disk": "{{space}} left on disk",
|
||||
"eta": "Conclusion {{eta}}",
|
||||
"calculating_eta": "Calculating remaining time…",
|
||||
"downloading_metadata": "Downloading metadata…",
|
||||
"checking_files": "Checking files…",
|
||||
"filter": "Filter repacks",
|
||||
"requirements": "System requirements",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Recommended",
|
||||
"no_minimum_requirements": "{{title}} doesn't provide minimum requirements information",
|
||||
"no_recommended_requirements": "{{title}} doesn't provide recommended requirements information",
|
||||
"paused_progress": "{{progress}} (Paused)",
|
||||
"paused": "Paused",
|
||||
"release_date": "Released on {{date}}",
|
||||
"publisher": "Published by {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copy link",
|
||||
@ -103,7 +102,9 @@
|
||||
"previous_screenshot": "Previous screenshot",
|
||||
"next_screenshot": "Next screenshot",
|
||||
"screenshot": "Screenshot {{number}}",
|
||||
"open_screenshot": "Open screenshot {{number}}"
|
||||
"open_screenshot": "Open screenshot {{number}}",
|
||||
"download_settings": "Download settings",
|
||||
"downloader": "Downloader"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@ -121,22 +122,19 @@
|
||||
"verifying": "Verifying…",
|
||||
"completed_at": "Completed in {{date}}",
|
||||
"completed": "Completed",
|
||||
"cancelled": "Cancelled",
|
||||
"removed": "Not downloaded",
|
||||
"download_again": "Download again",
|
||||
"cancel": "Cancel",
|
||||
"filter": "Filter downloaded games",
|
||||
"remove": "Remove",
|
||||
"downloading_metadata": "Downloading metadata…",
|
||||
"checking_files": "Checking files…",
|
||||
"starting_download": "Starting download…",
|
||||
"deleting": "Deleting installer…",
|
||||
"delete": "Remove installer",
|
||||
"remove_from_list": "Remove",
|
||||
"delete_modal_title": "Are you sure?",
|
||||
"delete_modal_description": "This will remove all the installation files from your computer",
|
||||
"install": "Install",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "Install"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads path",
|
||||
@ -146,15 +144,20 @@
|
||||
"enable_repack_list_notifications": "When a new repack is added",
|
||||
"telemetry": "Telemetry",
|
||||
"telemetry_description": "Enable anonymous usage statistics",
|
||||
"real_debrid_api_token_label": "Real Debrid API token",
|
||||
"quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray",
|
||||
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||
"quit_app_instead_hiding": "Don't hide Hydra when closing",
|
||||
"launch_with_system": "Launch Hydra on system start-up",
|
||||
"general": "General",
|
||||
"behavior": "Behavior",
|
||||
"enable_real_debrid": "Enable Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "You can get your API key <0>here</0>.",
|
||||
"save_changes": "Save changes"
|
||||
"real_debrid_api_token": "API Token",
|
||||
"enable_real_debrid": "Enable Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.",
|
||||
"real_debrid_invalid_token": "Invalid API token",
|
||||
"real_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_linked_message": "Account \"{{username}}\" linked",
|
||||
"save_changes": "Save changes",
|
||||
"changes_saved": "Changes successfully saved"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Ajustes",
|
||||
"my_library": "Mi biblioteca",
|
||||
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Analizando archivos…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
||||
"filter": "Buscar en la biblioteca",
|
||||
@ -29,13 +28,14 @@
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Descargas",
|
||||
"search_results": "Resultados de búsqueda",
|
||||
"settings": "Ajustes"
|
||||
"settings": "Ajustes",
|
||||
"version_available": "Version {{version}} disponible. Haz clic aquí para reiniciar e instalar."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Sin descargas en progreso",
|
||||
"downloading_metadata": "Descargando metadatos de {{title}}…",
|
||||
"checking_files": "Analizando archivos de {{title}} - ({{percentage}} completado)",
|
||||
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}"
|
||||
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Calculando tiempo restante…"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Siguiente página",
|
||||
@ -55,15 +55,15 @@
|
||||
"remove_from_list": "Quitar",
|
||||
"space_left_on_disk": "{{space}} restantes en el disco",
|
||||
"eta": "Tiempo restante: {{eta}}",
|
||||
"calculating_eta": "Calculando tiempo restante…",
|
||||
"downloading_metadata": "Descargando metadatos…",
|
||||
"checking_files": "Analizando archivos…",
|
||||
"filter": "Buscar repacks",
|
||||
"requirements": "Requisitos del Sistema",
|
||||
"minimum": "Mínimos",
|
||||
"recommended": "Recomendados",
|
||||
"no_minimum_requirements": "Sin requisitos mínimos para {{title}}",
|
||||
"no_recommended_requirements": "{{title}} no tiene requisitos recomendados",
|
||||
"paused_progress": "{{progress}} (Pausado)",
|
||||
"paused": "Pausado",
|
||||
"release_date": "Fecha de lanzamiento: {{date}}",
|
||||
"publisher": "Publicado por: {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copiar enlace",
|
||||
@ -102,7 +102,9 @@
|
||||
"previous_screenshot": "Anterior captura",
|
||||
"next_screenshot": "Siguiente captura",
|
||||
"screenshot": "Captura {{number}}",
|
||||
"open_screenshot": "Abrir captura {{number}}"
|
||||
"open_screenshot": "Abrir captura {{number}}",
|
||||
"download_settings": "Ajustes de descarga",
|
||||
"downloader": "Descargador"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activar Hydra",
|
||||
@ -120,22 +122,19 @@
|
||||
"verifying": "Verificando…",
|
||||
"completed_at": "Completado el {{date}}",
|
||||
"completed": "Completado",
|
||||
"cancelled": "Cancelado",
|
||||
"removed": "No descargado",
|
||||
"download_again": "Descargar de nuevo",
|
||||
"cancel": "Cancelar",
|
||||
"filter": "Buscar juegos descargados",
|
||||
"remove": "Eliminar",
|
||||
"downloading_metadata": "Descargando metadatos…",
|
||||
"checking_files": "Verificando archivos…",
|
||||
"starting_download": "Iniciando descarga…",
|
||||
"deleting": "Eliminando instalador…",
|
||||
"delete": "Eliminar instalador",
|
||||
"remove_from_list": "Eliminar",
|
||||
"delete_modal_title": "¿Estás seguro?",
|
||||
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
|
||||
"install": "Instalar",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "Instalar"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Ruta de descarga",
|
||||
@ -145,15 +144,20 @@
|
||||
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
||||
"telemetry": "Telemetría",
|
||||
"telemetry_description": "Habilitar recopilación de datos de manera anónima",
|
||||
"real_debrid_api_token_label": "Token API de Real Debrid",
|
||||
"real_debrid_api_token_label": "Token API de Real-Debrid",
|
||||
"quit_app_instead_hiding": "Salir de Hydra en vez de minimizar en la bandeja del sistema",
|
||||
"launch_with_system": "Iniciar Hydra al inicio del sistema",
|
||||
"general": "General",
|
||||
"behavior": "Otros",
|
||||
"enable_real_debrid": "Activar Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>.",
|
||||
"save_changes": "Guardar cambios"
|
||||
"real_debrid_api_token": "Token API",
|
||||
"enable_real_debrid": "Activar Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid es un descargador sin restricciones que te permite descargar archivos instantáneamente con la máxima velocidad de tu internet.",
|
||||
"real_debrid_invalid_token": "Token de API inválido",
|
||||
"real_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_linked_message": "Cuenta \"{{username}}\" vinculada",
|
||||
"save_changes": "Guardar cambios",
|
||||
"changes_saved": "Ajustes guardados exitosamente"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Descarga completada",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "تنظیمات",
|
||||
"my_library": "کتابخانهی من",
|
||||
"downloading_metadata": "{{title}} (در حال دانلود متادیتا...)",
|
||||
"checking_files": "{{title}} ({{percentage}} - در حال بررسی فایلها...)",
|
||||
"paused": "{{title}} (متوقف شده)",
|
||||
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
||||
"filter": "فیلتر کردن کتابخانه",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "دانلودی در حال انجام نیست",
|
||||
"downloading_metadata": "درحال دانلود متادیتاهای {{title}}…",
|
||||
"checking_files": "در حال چک کردن فایلهای {{title}}…. ({{percentage}} تکمیل شده)",
|
||||
"downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} فضا در دیسک باقیمانده",
|
||||
"eta": "اتمام {{eta}}",
|
||||
"downloading_metadata": "در حال دانلود متادیتاها…",
|
||||
"checking_files": "در حال چک کردن فایلها",
|
||||
"filter": "فیلترکردن ریپکها",
|
||||
"requirements": "سیستم مورد نیاز",
|
||||
"minimum": "حداقل",
|
||||
"recommended": "پیشنهادی",
|
||||
"no_minimum_requirements": "{{title}} اطلاعات حداقل سیستم مورد نیاز را فراهم نکرده",
|
||||
"no_recommended_requirements": "{{title}} اطلاعات پیشنهادی سیستم مورد نیاز را فراهم نکرده",
|
||||
"paused_progress": "{{progress}} (متوقف شده)",
|
||||
"release_date": "منتشر شده در {{date}}",
|
||||
"publisher": "منتشر شده توسط {{publisher}}",
|
||||
"copy_link_to_clipboard": "کپی لینک",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "در حال بازی",
|
||||
"change": "تغییر",
|
||||
"repacks_modal_description": "ریپک مورد نظر برای دانلود را انتخاب کنید",
|
||||
"downloads_path": "آدرس دانلودها",
|
||||
"select_folder_hint": "برای تغییر پوشهی پیشفرض به <0>Settings</0> بروید",
|
||||
"download_now": "الان دانلود کن",
|
||||
"installation_instructions": "دستورات نصب",
|
||||
@ -114,22 +109,18 @@
|
||||
"verifying": "در حال اعتبارسنجی…",
|
||||
"completed_at": "پایان یافته در {{date}}",
|
||||
"completed": "پایان یافته",
|
||||
"cancelled": "لغو شده",
|
||||
"download_again": "دانلود مجدد",
|
||||
"cancel": "لغو",
|
||||
"filter": "فیلتر بازیهای دانلود شده",
|
||||
"remove": "حذف",
|
||||
"downloading_metadata": "در حال دانلود متادیتاها…",
|
||||
"checking_files": "در حال چک کردن فایلها…",
|
||||
"starting_download": "در حال آغار دانلود…",
|
||||
"deleting": "در حال پاک کردن اینستالر…",
|
||||
"delete": "پاک کردن",
|
||||
"remove_from_list": "حذف",
|
||||
"delete_modal_title": "مطمئنی؟",
|
||||
"delete_modal_description": "این کار تمام فایلهای اینستالر را از کامپیوتر شما حذف میکند",
|
||||
"install": "نصف",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "تورنت"
|
||||
"install": "نصف"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "مسیر دانلودها",
|
||||
@ -139,13 +130,11 @@
|
||||
"enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد",
|
||||
"telemetry": "تلمتری",
|
||||
"telemetry_description": "فعال کردن آمارگیری استفاده ناشناس",
|
||||
"real_debrid_api_token_description": "توکن Real Debrid",
|
||||
"quit_app_instead_hiding": "به جای کوچک کردن، از هایدرا خارج شو",
|
||||
"launch_with_system": "زمانی که سیستم روشن میشود، هایدرا را باز کن",
|
||||
"general": "کلی",
|
||||
"behavior": "رفتار",
|
||||
"enable_real_debrid": "فعالسازی Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"enable_real_debrid": "فعالسازی Real-Debrid",
|
||||
"real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
||||
"save_changes": "ذخیره تغییرات"
|
||||
},
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Paramètres",
|
||||
"my_library": "Ma bibliothèque",
|
||||
"downloading_metadata": "{{title}} (Téléchargement des métadonnées…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Vérification des fichiers…)",
|
||||
"paused": "{{title}} (En pause)",
|
||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||
"filter": "Filtrer la bibliothèque",
|
||||
@ -30,7 +29,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Aucun téléchargement en cours",
|
||||
"downloading_metadata": "Téléchargement des métadonnées de {{title}}…",
|
||||
"checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)",
|
||||
"downloading": "Téléchargement de {{title}}… ({{percentage}} terminé) - Fin dans {{eta}} - {{speed}}"
|
||||
},
|
||||
"game_details": {
|
||||
@ -47,14 +45,12 @@
|
||||
"space_left_on_disk": "{{space}} restant sur le disque",
|
||||
"eta": "Fin dans {{eta}}",
|
||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
||||
"checking_files": "Vérification des fichiers…",
|
||||
"filter": "Filtrer les repacks",
|
||||
"requirements": "Configuration requise",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Recommandée",
|
||||
"no_minimum_requirements": "{{title}} ne fournit pas d'informations sur les configurations minimales",
|
||||
"no_recommended_requirements": "{{title}} ne fournit pas d'informations sur les configurations recommandées",
|
||||
"paused_progress": "{{progress}} (En pause)",
|
||||
"release_date": "Sorti le {{date}}",
|
||||
"publisher": "Édité par {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copier le lien",
|
||||
@ -93,13 +89,11 @@
|
||||
"verifying": "Vérification en cours…",
|
||||
"completed_at": "Terminé en {{date}}",
|
||||
"completed": "Terminé",
|
||||
"cancelled": "Annulé",
|
||||
"download_again": "Télécharger à nouveau",
|
||||
"cancel": "Annuler",
|
||||
"filter": "Filtrer les jeux téléchargés",
|
||||
"remove": "Supprimer",
|
||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
||||
"checking_files": "Vérification des fichiers…",
|
||||
"starting_download": "Démarrage du téléchargement…",
|
||||
"remove_from_list": "Retirer",
|
||||
"delete": "Supprimer le programme d'installation",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Beállítások",
|
||||
"my_library": "Könyvtáram",
|
||||
"downloading_metadata": "{{title}} (Metadata letöltése…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Fájlok ellenőrzése…)",
|
||||
"paused": "{{title}} (Szünet)",
|
||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||
"filter": "Könyvtár szűrése",
|
||||
@ -30,7 +29,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
|
||||
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
||||
"checking_files": "{{title}} fájlok ellenőrzése… ({{percentage}} kész)",
|
||||
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -52,14 +50,12 @@
|
||||
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
||||
"eta": "Befejezés {{eta}}",
|
||||
"downloading_metadata": "Metaadatok letöltése…",
|
||||
"checking_files": "Fájlok ellenőrzése…",
|
||||
"filter": "Repackek szűrése",
|
||||
"requirements": "Rendszerkövetelmények",
|
||||
"minimum": "Minimális",
|
||||
"recommended": "Ajánlott",
|
||||
"no_minimum_requirements": "{{title}} nem tartalmaz információt a minimális követelményekről",
|
||||
"no_recommended_requirements": "{{title}} nem tartalmaz információt az ajánlott követelményekről",
|
||||
"paused_progress": "{{progress}} (Szünetel)",
|
||||
"release_date": "Megjelenés: {{date}}",
|
||||
"publisher": "Kiadta: {{publisher}}",
|
||||
"copy_link_to_clipboard": "Link másolása",
|
||||
@ -82,7 +78,6 @@
|
||||
"playing_now": "Jelenleg játszva",
|
||||
"change": "Változtatás",
|
||||
"repacks_modal_description": "Choose the repack you want to download",
|
||||
"downloads_path": "Letöltések helye",
|
||||
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
|
||||
"download_now": "Töltsd le most"
|
||||
},
|
||||
@ -102,13 +97,11 @@
|
||||
"verifying": "Ellenőrzés…",
|
||||
"completed_at": "Befejezve {{date}}-kor",
|
||||
"completed": "Befejezve",
|
||||
"cancelled": "Megszakítva",
|
||||
"download_again": "Újra letöltés",
|
||||
"cancel": "Mégse",
|
||||
"filter": "Letöltött játékok szűrése",
|
||||
"remove": "Eltávolítás",
|
||||
"downloading_metadata": "Metaadatok letöltése…",
|
||||
"checking_files": "Fájlok ellenőrzése…",
|
||||
"starting_download": "Letöltés indítása…",
|
||||
"deleting": "Telepítő törlése…",
|
||||
"delete": "Telepítő eltávolítása",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Pengaturan",
|
||||
"my_library": "Koleksi saya",
|
||||
"downloading_metadata": "{{title}} (Mengunduh metadata…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Memeriksa file…)",
|
||||
"paused": "{{title}} (Terhenti)",
|
||||
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
||||
"filter": "Filter koleksi",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Tidak ada unduhan berjalan",
|
||||
"downloading_metadata": "Mengunduh metadata {{title}}...",
|
||||
"checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)",
|
||||
"downloading": "Mengunduh {{title}}… ({{percentage}} selesai) - Perkiraan {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} tersisa pada disk",
|
||||
"eta": "Perkiraan {{eta}}",
|
||||
"downloading_metadata": "Mengunduh metadata…",
|
||||
"checking_files": "Memeriksa file…",
|
||||
"filter": "Saring repacks",
|
||||
"requirements": "Keperluan sistem",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Rekomendasi",
|
||||
"no_minimum_requirements": "{{title}} Tidak ada informasi kebutuhan sistem",
|
||||
"no_recommended_requirements": "{{title}} Tidak ada informasi rekomendasi kebutuhan sistem",
|
||||
"paused_progress": "{{progress}} (Terhenti)",
|
||||
"release_date": "Dirilis pada {{date}}",
|
||||
"publisher": "Dipublikasikan oleh {{publisher}}",
|
||||
"copy_link_to_clipboard": "Salin tautan",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "Memainkan sekarang",
|
||||
"change": "Ubah",
|
||||
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
|
||||
"downloads_path": "Lokasi Unduhan",
|
||||
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
|
||||
"download_now": "Unduh sekarang",
|
||||
"installation_instructions": "Instruksi Instalasi",
|
||||
@ -114,13 +109,11 @@
|
||||
"verifying": "Memeriksa…",
|
||||
"completed_at": "Selesai pada {{date}}",
|
||||
"completed": "Selesai",
|
||||
"cancelled": "Dibatalkan",
|
||||
"download_again": "Unduh lagi",
|
||||
"cancel": "Batalkan",
|
||||
"filter": "Saring game yang diunduh",
|
||||
"remove": "Hapus",
|
||||
"downloading_metadata": "Mengunduh metadata…",
|
||||
"checking_files": "Memeriksa file…",
|
||||
"starting_download": "Memulai unduhan…",
|
||||
"deleting": "Menghapus file instalasi…",
|
||||
"delete": "Hapus file instalasi",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Impostazioni",
|
||||
"my_library": "La mia libreria",
|
||||
"downloading_metadata": "{{title}} (Scaricamento metadati…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Verifica file…)",
|
||||
"paused": "{{title}} (In pausa)",
|
||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||
"filter": "Filtra libreria",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Nessun download in corso",
|
||||
"downloading_metadata": "Scaricamento metadati di {{title}}…",
|
||||
"checking_files": "Verifica file di {{title}}… ({{percentage}} completato)",
|
||||
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} rimasto sul disco",
|
||||
"eta": "Conclusione {{eta}}",
|
||||
"downloading_metadata": "Scaricamento metadati…",
|
||||
"checking_files": "Verifica file…",
|
||||
"filter": "Filtra repack",
|
||||
"requirements": "Requisiti di sistema",
|
||||
"minimum": "Minimi",
|
||||
"recommended": "Consigliati",
|
||||
"no_minimum_requirements": "{{title}} non fornisce informazioni sui requisiti minimi",
|
||||
"no_recommended_requirements": "{{title}} non fornisce informazioni sui requisiti consigliati",
|
||||
"paused_progress": "{{progress}} (In pausa)",
|
||||
"release_date": "Rilasciato il {{date}}",
|
||||
"publisher": "Pubblicato da {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copia link",
|
||||
@ -120,22 +116,18 @@
|
||||
"verifying": "Verifica…",
|
||||
"completed_at": "Completato in {{date}}",
|
||||
"completed": "Completato",
|
||||
"cancelled": "Annullato",
|
||||
"download_again": "Scarica di nuovo",
|
||||
"cancel": "Annulla",
|
||||
"filter": "Filtra giochi scaricati",
|
||||
"remove": "Rimuovi",
|
||||
"downloading_metadata": "Scaricamento metadati…",
|
||||
"checking_files": "Verifica file…",
|
||||
"starting_download": "Avvio download…",
|
||||
"deleting": "Eliminazione dell'installer…",
|
||||
"delete": "Rimuovi installer",
|
||||
"remove_from_list": "Rimuovi",
|
||||
"delete_modal_title": "Sei sicuro?",
|
||||
"delete_modal_description": "Questo rimuoverà tutti i file di installazione dal tuo computer",
|
||||
"install": "Installa",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "Installa"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Percorso dei download",
|
||||
@ -151,8 +143,7 @@
|
||||
"general": "Generale",
|
||||
"behavior": "Comportamento",
|
||||
"enable_real_debrid": "Abilita Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>.",
|
||||
"real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
||||
"save_changes": "Salva modifiche"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "설정",
|
||||
"my_library": "내 라이브러리",
|
||||
"downloading_metadata": "{{title}} (메타데이터 다운로드 중…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - 파일 검사 중…)",
|
||||
"paused": "{{title}} (일시 정지됨)",
|
||||
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
||||
"filter": "라이브러리 정렬",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "진행중인 다운로드 없음",
|
||||
"downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…",
|
||||
"checking_files": "{{title}}의 파일들을 검사 중… ({{percentage}} 완료)",
|
||||
"downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "여유 저장 용량 {{space}} 남음",
|
||||
"eta": "완료까지 {{eta}}",
|
||||
"downloading_metadata": "메타데이터 다운로드 중…",
|
||||
"checking_files": "파일 검사 중…",
|
||||
"filter": "리팩들을 다음과 같이 정렬하기",
|
||||
"requirements": "시스템 사양",
|
||||
"minimum": "최저 사양",
|
||||
"recommended": "권장 사양",
|
||||
"no_minimum_requirements": "{{title}}의 최저 사양을 제공받지 못 함",
|
||||
"no_recommended_requirements": "{{title}}의 권장 사양을 제공받지 못 함",
|
||||
"paused_progress": "{{progress}} (일시 정지)",
|
||||
"release_date": "{{date}}에 발매됨",
|
||||
"publisher": "{{publisher}} 배급",
|
||||
"copy_link_to_clipboard": "링크 복사하기",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "현재 플레이 중",
|
||||
"change": "바꾸기",
|
||||
"repacks_modal_description": "다운로드 할 리팩을 선택해 주세요",
|
||||
"downloads_path": "다운로드 경로",
|
||||
"select_folder_hint": "기본 폴더를 바꾸려면 <0>설정</0>으로 가세요",
|
||||
"download_now": "지금 다운로드",
|
||||
"installation_instructions": "설치 방법",
|
||||
@ -114,22 +109,18 @@
|
||||
"verifying": "검증중…",
|
||||
"completed_at": "{{date}}에 완료됨",
|
||||
"completed": "완료됨",
|
||||
"cancelled": "취소됨",
|
||||
"download_again": "다시 다운로드 하기",
|
||||
"cancel": "취소",
|
||||
"filter": "다운로드 된 게임들을 정렬하기",
|
||||
"remove": "제거하기",
|
||||
"downloading_metadata": "메타데이터 다운로드 중…",
|
||||
"checking_files": "파일 검사 중…",
|
||||
"starting_download": "다운로드 개시 중…",
|
||||
"deleting": "인스톨러 삭제 중…",
|
||||
"delete": "인스톨러 삭제하기",
|
||||
"remove_from_list": "제거하기",
|
||||
"delete_modal_title": "정말로 하시겠습니까?",
|
||||
"delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다",
|
||||
"install": "설치",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "설치"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "다운로드 경로",
|
||||
@ -139,13 +130,11 @@
|
||||
"enable_repack_list_notifications": "새 리팩이 추가되었을 때",
|
||||
"telemetry": "자동 데이터 수집",
|
||||
"telemetry_description": "익명 사용 통계를 활성화",
|
||||
"real_debrid_api_token_description": "Real Debrid API 토큰",
|
||||
"quit_app_instead_hiding": "작업 표시줄 트레이로 최소화하는 대신 Hydra를 종료",
|
||||
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
|
||||
"general": "일반",
|
||||
"behavior": "행동",
|
||||
"enable_real_debrid": "Real Debrid 활성화",
|
||||
"real_debrid": "Real Debrid",
|
||||
"enable_real_debrid": "Real-Debrid 활성화",
|
||||
"real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
||||
"save_changes": "변경 사항 저장"
|
||||
},
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Instellingen",
|
||||
"my_library": "Mijn Bibliotheek",
|
||||
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Folders checken…)",
|
||||
"paused": "{{title}} (Gepauzeerd)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filter Bibliotheek",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Geen Downloads bezig",
|
||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)",
|
||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} Over op schijf",
|
||||
"eta": "Conclusie {{eta}}",
|
||||
"downloading_metadata": "Downloading metadata…",
|
||||
"checking_files": "Files nakijken…",
|
||||
"filter": "Filter repacks",
|
||||
"requirements": "Systeem vereisten",
|
||||
"minimum": "Minimaal",
|
||||
"recommended": "Aanbevolen",
|
||||
"no_minimum_requirements": "{{title}} biedt geen informatie over de minimale vereisten",
|
||||
"no_recommended_requirements": "{{title}} biedt geen informatie over aanbevolen vereisten",
|
||||
"paused_progress": "{{progress}} (Paused)",
|
||||
"release_date": "Uitgebracht op {{date}}",
|
||||
"publisher": "Gepubliceerd door {{publisher}}",
|
||||
"copy_link_to_clipboard": "Kopieer link",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "Speel nu",
|
||||
"change": "Verander",
|
||||
"repacks_modal_description": "Kies de herverpakking die u wilt downloaden",
|
||||
"downloads_path": "Downloads path",
|
||||
"select_folder_hint": "Om de standaardmap te wijzigen, gaat u naar <0>instellingen</0>",
|
||||
"download_now": "Download nu",
|
||||
"installation_instructions": "Installatie instructies",
|
||||
@ -114,22 +109,18 @@
|
||||
"verifying": "Verifiëren…",
|
||||
"completed_at": "Voltooid binnen {{date}}",
|
||||
"completed": "Voltooid",
|
||||
"cancelled": "Geannuleerd",
|
||||
"download_again": "Opnieuw downloaden",
|
||||
"cancel": "Annuleren",
|
||||
"filter": "Filter gedownloade games",
|
||||
"remove": "Verwijderen",
|
||||
"downloading_metadata": "Metagegevens downloaden",
|
||||
"checking_files": "Bestanden controleren",
|
||||
"starting_download": "download starten",
|
||||
"deleting": "Installatieprogramma verwijderen…",
|
||||
"delete": "Installatieprogramma verwijderen",
|
||||
"remove_from_list": "Verwijderen",
|
||||
"delete_modal_title": "Weet je het zeker?",
|
||||
"delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd",
|
||||
"install": "Installeren",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "Installeren"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloadpad",
|
||||
@ -139,13 +130,12 @@
|
||||
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
|
||||
"telemetry": "Telemetrie",
|
||||
"telemetry_description": "Schakel anonieme gebruiksstatistieken in",
|
||||
"real_debrid_api_token_label": "Real Debrid API token",
|
||||
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||
"quit_app_instead_hiding": "Sluit Hydra af in plaats van te minimaliseren naar de lade",
|
||||
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
|
||||
"general": "Algemeen",
|
||||
"behavior": "Gedrag",
|
||||
"enable_real_debrid": "Enable Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"enable_real_debrid": "Enable Real-Debrid",
|
||||
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
||||
"save_changes": "Wijzigingen opslaan"
|
||||
},
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Ustawienia",
|
||||
"my_library": "Moja biblioteka",
|
||||
"downloading_metadata": "{{title}} (Pobieranie metadata…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Sprawdzanie plików…)",
|
||||
"paused": "{{title}} (Zatrzymano)",
|
||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||
"filter": "Filtruj biblioteke",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Brak pobierań w toku",
|
||||
"downloading_metadata": "Pobieranie {{title}} metadata…",
|
||||
"checking_files": "Sprawdzanie {{title}} plików… (ukończone w {{percentage}})",
|
||||
"downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} wolnego na dysku",
|
||||
"eta": "Podsumowanie {{eta}}",
|
||||
"downloading_metadata": "Pobieranie metadata…",
|
||||
"checking_files": "Sprawdzanie plików…",
|
||||
"filter": "Filtruj repacki",
|
||||
"requirements": "Wymagania systemowe",
|
||||
"minimum": "Minimalne",
|
||||
"recommended": "Zalecane",
|
||||
"no_minimum_requirements": "{{title}} nie zawiera informacji o minimalnych wymaganiach",
|
||||
"no_recommended_requirements": "{{title}} nie zawiera informacji o zalecanych wymaganiach",
|
||||
"paused_progress": "{{progress}} (Zatrzymano)",
|
||||
"release_date": "Wydano w {{date}}",
|
||||
"publisher": "Opublikowany przez {{publisher}}",
|
||||
"copy_link_to_clipboard": "Kopiuj łącze",
|
||||
@ -120,22 +116,18 @@
|
||||
"verifying": "Weryfikowanie…",
|
||||
"completed_at": "Zakończono w {{date}}",
|
||||
"completed": "Zakończono",
|
||||
"cancelled": "Anulowano",
|
||||
"download_again": "Pobierz ponownie",
|
||||
"cancel": "Anuluj",
|
||||
"filter": "Filtruj pobrane gry",
|
||||
"remove": "Usuń",
|
||||
"downloading_metadata": "Pobieranie metadata…",
|
||||
"checking_files": "Sprawdzanie plików…",
|
||||
"starting_download": "Rozpoczęto pobieranie…",
|
||||
"deleting": "Usuwanie instalatora…",
|
||||
"delete": "Usuń instalator",
|
||||
"remove_from_list": "Usuń",
|
||||
"delete_modal_title": "Czy na pewno?",
|
||||
"delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera",
|
||||
"install": "Instaluj",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "Instaluj"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Ścieżka pobierania",
|
||||
@ -145,14 +137,13 @@
|
||||
"enable_repack_list_notifications": "Gdy dodawany jest nowy repack",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Włącz anonimowe statystyki użycia",
|
||||
"real_debrid_api_token_label": "Real Debrid API token",
|
||||
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||
"quit_app_instead_hiding": "Zamknij Hydr zamiast minimalizować do zasobnika",
|
||||
"launch_with_system": "Uruchom Hydra przy starcie systemu",
|
||||
"general": "Ogólne",
|
||||
"behavior": "Zachowania",
|
||||
"enable_real_debrid": "Włącz Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>.",
|
||||
"enable_real_debrid": "Włącz Real-Debrid",
|
||||
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
||||
"save_changes": "Zapisz zmiany"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -9,10 +9,9 @@
|
||||
"sidebar": {
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Downloads",
|
||||
"settings": "Configurações",
|
||||
"settings": "Ajustes",
|
||||
"my_library": "Minha biblioteca",
|
||||
"downloading_metadata": "{{title}} (Baixando metadados…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Verificando arquivos…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
"downloading": "{{title}} ({{percentage}} - Baixando…)",
|
||||
"filter": "Filtrar biblioteca",
|
||||
@ -28,15 +27,15 @@
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Downloads",
|
||||
"search_results": "Resultados da busca",
|
||||
"settings": "Configurações",
|
||||
"settings": "Ajustes",
|
||||
"home": "Início",
|
||||
"version_available": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Sem downloads em andamento",
|
||||
"downloading_metadata": "Baixando metadados de {{title}}…",
|
||||
"checking_files": "Verificando arquivos de {{title}}… ({{percentage}} completo)",
|
||||
"downloading": "Baixando {{title}}… ({{percentage}} completo) - Conclusão {{eta}} - {{speed}}"
|
||||
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Ver opções de download",
|
||||
@ -51,15 +50,15 @@
|
||||
"remove_from_list": "Remover",
|
||||
"space_left_on_disk": "{{space}} livres em disco",
|
||||
"eta": "Conclusão {{eta}}",
|
||||
"calculating_eta": "Calculando tempo restante…",
|
||||
"downloading_metadata": "Baixando metadados…",
|
||||
"checking_files": "Verificando arquivos…",
|
||||
"filter": "Filtrar repacks",
|
||||
"requirements": "Requisitos do sistema",
|
||||
"minimum": "Mínimos",
|
||||
"recommended": "Recomendados",
|
||||
"no_minimum_requirements": "{{title}} não possui informações de requisitos mínimos",
|
||||
"no_recommended_requirements": "{{title}} não possui informações de requisitos recomendados",
|
||||
"paused_progress": "{{progress}} (Pausado)",
|
||||
"paused": "Pausado",
|
||||
"release_date": "Lançado em {{date}}",
|
||||
"publisher": "Publicado por {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copiar link",
|
||||
@ -83,8 +82,8 @@
|
||||
"playing_now": "Jogando agora",
|
||||
"change": "Mudar",
|
||||
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
|
||||
"select_folder_hint": "Para trocar a pasta padrão, acesse a <0>Tela de Configurações</0>",
|
||||
"download_now": "Baixe agora",
|
||||
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes</0>",
|
||||
"download_now": "Iniciar download",
|
||||
"installation_instructions": "Instruções de Instalação",
|
||||
"installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo",
|
||||
"online_fix_instruction": "Jogos OnlineFix precisam de uma senha para serem extraídos. Quando solicitado, utilize a seguinte senha:",
|
||||
@ -99,7 +98,9 @@
|
||||
"previous_screenshot": "Captura de tela anterior",
|
||||
"next_screenshot": "Próxima captura de tela",
|
||||
"screenshot": "Captura de tela {{number}}",
|
||||
"open_screenshot": "Ver captura de tela {{number}}"
|
||||
"open_screenshot": "Ver captura de tela {{number}}",
|
||||
"download_settings": "Ajustes do download",
|
||||
"downloader": "Downloader"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@ -117,22 +118,19 @@
|
||||
"verifying": "Verificando…",
|
||||
"completed_at": "Concluído em {{date}}",
|
||||
"completed": "Concluído",
|
||||
"cancelled": "Cancelado",
|
||||
"removed": "Não baixado",
|
||||
"download_again": "Baixar novamente",
|
||||
"cancel": "Cancelar",
|
||||
"filter": "Filtrar jogos baixados",
|
||||
"remove": "Remover",
|
||||
"downloading_metadata": "Baixando metadados…",
|
||||
"checking_files": "Verificando arquivos…",
|
||||
"starting_download": "Iniciando download…",
|
||||
"remove_from_list": "Remover",
|
||||
"delete": "Remover instalador",
|
||||
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
||||
"delete_modal_title": "Tem certeza?",
|
||||
"deleting": "Excluindo instalador…",
|
||||
"install": "Instalar",
|
||||
"torrent": "Torrent",
|
||||
"real_debrid": "Real Debrid"
|
||||
"install": "Instalar"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
@ -142,15 +140,20 @@
|
||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Habilitar estatísticas de uso anônimas",
|
||||
"real_debrid_api_token_label": "Token de API do Real Debrid",
|
||||
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo",
|
||||
"launch_with_system": "Iniciar aplicativo na inicialização do sistema",
|
||||
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
||||
"quit_app_instead_hiding": "Encerrar o Hydra ao invés de minimizá-lo ao fechar",
|
||||
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
||||
"general": "Geral",
|
||||
"behavior": "Comportamento",
|
||||
"enable_real_debrid": "Habilitar Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui</0>.",
|
||||
"save_changes": "Salvar mudanças"
|
||||
"real_debrid_api_token": "Token de API",
|
||||
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||
"real_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_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_linked_message": "Conta \"{{username}}\" vinculada",
|
||||
"save_changes": "Salvar mudanças",
|
||||
"changes_saved": "Ajustes salvos com sucesso"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
@ -177,11 +180,5 @@
|
||||
},
|
||||
"modal": {
|
||||
"close": "Botão de fechar"
|
||||
},
|
||||
"splash": {
|
||||
"downloading_version": "Baixando versão {{version}}",
|
||||
"searching_updates": "Buscando atualizações",
|
||||
"update_found": "Versão {{version}} encontrada",
|
||||
"restarting_and_applying": "Reiniciando e aplicando atualização"
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Настройки",
|
||||
"my_library": "Библиотека",
|
||||
"downloading_metadata": "{{title}} (Загрузка метаданных…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Проверка файлов…)",
|
||||
"paused": "{{title}} (Приостановлено)",
|
||||
"downloading": "{{title}} ({{percentage}} - Загрузка…)",
|
||||
"filter": "Фильтр библиотеки",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Нет активных загрузок",
|
||||
"downloading_metadata": "Загрузка метаданных {{title}}…",
|
||||
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
|
||||
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} свободно на диске",
|
||||
"eta": "Окончание {{eta}}",
|
||||
"downloading_metadata": "Загрузка метаданных…",
|
||||
"checking_files": "Проверка файлов…",
|
||||
"filter": "Фильтр репаков",
|
||||
"requirements": "Системные требования",
|
||||
"minimum": "Минимальные",
|
||||
"recommended": "Рекомендуемые",
|
||||
"no_minimum_requirements": "Для {{title}} не указаны минимальные требования",
|
||||
"no_recommended_requirements": "Для {{title}} не указаны рекомендуемые требования",
|
||||
"paused_progress": "{{progress}} (Приостановлено)",
|
||||
"release_date": "Выпущено {{date}}",
|
||||
"publisher": "Издатель {{publisher}}",
|
||||
"copy_link_to_clipboard": "Копировать ссылку",
|
||||
@ -120,22 +116,18 @@
|
||||
"verifying": "Проверка…",
|
||||
"completed_at": "Завершено в {{date}}",
|
||||
"completed": "Завершено",
|
||||
"cancelled": "Отменено",
|
||||
"download_again": "Загрузить снова",
|
||||
"cancel": "Отменить",
|
||||
"filter": "Фильтр загруженных игр",
|
||||
"remove": "Удалить",
|
||||
"downloading_metadata": "Загрузка метаданных…",
|
||||
"checking_files": "Проверка файлов…",
|
||||
"starting_download": "Начало загрузки…",
|
||||
"deleting": "Удаление установщика…",
|
||||
"delete": "Удалить установщик",
|
||||
"remove_from_list": "Удалить",
|
||||
"delete_modal_title": "Вы уверены?",
|
||||
"delete_modal_description": "Это удалит все установщики с вашего компьютера",
|
||||
"install": "Установить",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
"install": "Установить"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Путь загрузок",
|
||||
@ -145,14 +137,13 @@
|
||||
"enable_repack_list_notifications": "При добавлении нового репака",
|
||||
"telemetry": "Телеметрия",
|
||||
"telemetry_description": "Отправлять анонимную статистику использования",
|
||||
"real_debrid_api_token_label": "Real Debrid API-токен",
|
||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||
"quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей",
|
||||
"launch_with_system": "Запуск Hydra вместе с системой",
|
||||
"general": "Основные",
|
||||
"behavior": "Поведение",
|
||||
"enable_real_debrid": "Включить Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>.",
|
||||
"enable_real_debrid": "Включить Real-Debrid",
|
||||
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
||||
"save_changes": "Сохранить изменения"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Ayarlar",
|
||||
"my_library": "Kütüphane",
|
||||
"downloading_metadata": "{{title}} (Metadata indiriliyor…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Dosyalar kontrol ediliyor…)",
|
||||
"paused": "{{title}} (Duraklatıldı)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphaneyi filtrele",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "İndirilen bir şey yok",
|
||||
"downloading_metadata": "{{title}} metadatası indiriliyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "Diskte {{space}} yer kaldı",
|
||||
"eta": "Bitiş {{eta}}",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"checking_files": "Dosyalar kontrol ediliyor…",
|
||||
"filter": "Repackleri filtrele",
|
||||
"requirements": "Sistem gereksinimleri",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Önerilen",
|
||||
"no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor",
|
||||
"no_recommended_requirements": "{{title}} önerilen sistem gereksinim bilgilerini karşılamıyor",
|
||||
"paused_progress": "{{progress}} (Duraklatıldı)",
|
||||
"release_date": "{{date}} tarihinde çıktı",
|
||||
"publisher": "{{publisher}} tarihinde yayınlandı",
|
||||
"copy_link_to_clipboard": "Link'i kopyala",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "Şimdi oynanıyor",
|
||||
"change": "Değiştir",
|
||||
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
|
||||
"downloads_path": "İndirme yolu",
|
||||
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
|
||||
"download_now": "Şimdi",
|
||||
"installation_instructions": "Kurulum",
|
||||
@ -114,13 +109,11 @@
|
||||
"verifying": "Doğrulanıyor…",
|
||||
"completed_at": "{{date}} tarihinde tamamlanacak",
|
||||
"completed": "Tamamlandı",
|
||||
"cancelled": "İptal edildi",
|
||||
"download_again": "Tekrar indir",
|
||||
"cancel": "İptal et",
|
||||
"filter": "Yüklü oyunları filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"checking_files": "Dosyalar kontrol ediliyor…",
|
||||
"starting_download": "İndirme başlatılıyor…",
|
||||
"deleting": "Installer siliniyor…",
|
||||
"delete": "Installer'ı sil",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "Налаштування",
|
||||
"my_library": "Бібліотека",
|
||||
"downloading_metadata": "{{title}} (Завантаження метаданих…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - Перевірка файлів…)",
|
||||
"paused": "{{title}} (Призупинено)",
|
||||
"downloading": "{{title}} ({{percentage}} - Завантаження…)",
|
||||
"filter": "Фільтр бібліотеки",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Немає активних завантажень",
|
||||
"downloading_metadata": "Завантаження метаданих {{title}}…",
|
||||
"checking_files": "Перевірка файлів {{title}}… ({{percentage}} завершено)",
|
||||
"downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "{{space}} вільно на диску",
|
||||
"eta": "Закінчення {{eta}}",
|
||||
"downloading_metadata": "Завантаження метаданих…",
|
||||
"checking_files": "Перевірка файлів…",
|
||||
"filter": "Фільтр репаків",
|
||||
"requirements": "Системні вимоги",
|
||||
"minimum": "Мінімальні",
|
||||
"recommended": "Рекомендовані",
|
||||
"no_minimum_requirements": "Для {{title}} не вказані мінімальні вимоги",
|
||||
"no_recommended_requirements": "Для {{title}} не вказані рекомендовані вимоги",
|
||||
"paused_progress": "{{progress}} (Призупинено)",
|
||||
"release_date": "Випущено {{date}}",
|
||||
"publisher": "Видавець {{publisher}}",
|
||||
"copy_link_to_clipboard": "Скопіювати посилання",
|
||||
@ -86,7 +82,6 @@
|
||||
"playing_now": "Поточна гра",
|
||||
"change": "Змінити",
|
||||
"repacks_modal_description": "Виберіть репак, який хочете завантажити",
|
||||
"downloads_path": "Шлях завантажень",
|
||||
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
|
||||
"download_now": "Завантажити зараз",
|
||||
"installation_instructions": "Інструкція зі встановлення",
|
||||
@ -114,13 +109,11 @@
|
||||
"verifying": "Перевірка…",
|
||||
"completed_at": "Завершено в {{date}}",
|
||||
"completed": "Завершено",
|
||||
"cancelled": "Скасовано",
|
||||
"download_again": "Завантажити знову",
|
||||
"cancel": "Скасувати",
|
||||
"filter": "Фільтр завантажених ігор",
|
||||
"remove": "Видалити",
|
||||
"downloading_metadata": "Завантаження метаданих…",
|
||||
"checking_files": "Перевірка файлів…",
|
||||
"starting_download": "Початок завантаження…",
|
||||
"deleting": "Видалення інсталятора…",
|
||||
"delete": "Видалити інсталятор",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"settings": "设置",
|
||||
"my_library": "我的游戏库",
|
||||
"downloading_metadata": "{{title}} (正在下载元数据…)",
|
||||
"checking_files": "{{title}} ({{percentage}} - 正在检查文件…)",
|
||||
"paused": "{{title}} (已暂停)",
|
||||
"downloading": "{{title}} ({{percentage}} - 正在下载…)",
|
||||
"filter": "筛选游戏库",
|
||||
@ -34,7 +33,6 @@
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "没有正在进行的下载",
|
||||
"downloading_metadata": "正在下载{{title}}的元数据…",
|
||||
"checking_files": "正在检查{{title}}的文件… ({{percentage}}完成)",
|
||||
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}"
|
||||
},
|
||||
"catalogue": {
|
||||
@ -56,14 +54,12 @@
|
||||
"space_left_on_disk": "磁盘剩余空间{{space}}",
|
||||
"eta": "预计完成时间{{eta}}",
|
||||
"downloading_metadata": "正在下载元数据…",
|
||||
"checking_files": "正在检查文件…",
|
||||
"filter": "筛选重打包",
|
||||
"requirements": "配置要求",
|
||||
"minimum": "最低要求",
|
||||
"recommended": "推荐要求",
|
||||
"no_minimum_requirements": "{{title}}没有提供最低要求信息",
|
||||
"no_recommended_requirements": "{{title}}没有提供推荐要求信息",
|
||||
"paused_progress": "{{progress}} (已暂停)",
|
||||
"release_date": "发布于{{date}}",
|
||||
"publisher": "发行商{{publisher}}",
|
||||
"copy_link_to_clipboard": "复制链接",
|
||||
@ -86,9 +82,7 @@
|
||||
"playing_now": "正在游戏中",
|
||||
"change": "更改",
|
||||
"repacks_modal_description": "选择您想要下载的重打包",
|
||||
"downloads_path": "下载路径",
|
||||
"select_folder_hint": "要更改默认文件夹,请访问",
|
||||
"settings": "设置",
|
||||
"download_now": "立即下载",
|
||||
"installation_instructions": "安装说明",
|
||||
"installation_instructions_description": "安装这个游戏需要额外的步骤",
|
||||
@ -118,22 +112,18 @@
|
||||
"verifying": "正在验证…",
|
||||
"completed_at": "完成于{{date}}",
|
||||
"completed": "已完成",
|
||||
"cancelled": "已取消",
|
||||
"download_again": "再次下载",
|
||||
"cancel": "取消",
|
||||
"filter": "筛选已下载游戏",
|
||||
"remove": "移除",
|
||||
"downloading_metadata": "正在下载元数据…",
|
||||
"checking_files": "正在检查文件…",
|
||||
"starting_download": "开始下载…",
|
||||
"deleting": "正在删除安装程序…",
|
||||
"delete": "移除安装程序",
|
||||
"remove_from_list": "移除",
|
||||
"delete_modal_title": "您确定吗?",
|
||||
"delete_modal_description": "这将从您的电脑上移除所有的安装文件",
|
||||
"install": "安装",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "种子"
|
||||
"install": "安装"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "下载路径",
|
||||
@ -143,13 +133,11 @@
|
||||
"enable_repack_list_notifications": "添加新重打包时",
|
||||
"telemetry": "遥测",
|
||||
"telemetry_description": "启用匿名使用统计",
|
||||
"real_debrid_api_token_description": "Real Debrid API密钥",
|
||||
"behavior": "行为",
|
||||
"general": "常规",
|
||||
"quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘",
|
||||
"launch_with_system": "随系统启动时运行应用程序",
|
||||
"enable_real_debrid": "启用 Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"enable_real_debrid": "启用 Real-Debrid",
|
||||
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
||||
"save_changes": "保存更改"
|
||||
},
|
||||
|
80
src/main/declaration.d.ts
vendored
Normal file
80
src/main/declaration.d.ts
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
declare module "aria2" {
|
||||
export type Aria2Status =
|
||||
| "active"
|
||||
| "waiting"
|
||||
| "paused"
|
||||
| "error"
|
||||
| "complete"
|
||||
| "removed";
|
||||
|
||||
export interface StatusResponse {
|
||||
gid: string;
|
||||
status: Aria2Status;
|
||||
totalLength: string;
|
||||
completedLength: string;
|
||||
uploadLength: string;
|
||||
bitfield: string;
|
||||
downloadSpeed: string;
|
||||
uploadSpeed: string;
|
||||
infoHash?: string;
|
||||
numSeeders?: string;
|
||||
seeder?: boolean;
|
||||
pieceLength: string;
|
||||
numPieces: string;
|
||||
connections: string;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
followedBy?: string[];
|
||||
following: string;
|
||||
belongsTo: string;
|
||||
dir: string;
|
||||
files: {
|
||||
path: string;
|
||||
length: string;
|
||||
completedLength: string;
|
||||
selected: string;
|
||||
}[];
|
||||
bittorrent?: {
|
||||
announceList: string[][];
|
||||
comment: string;
|
||||
creationDate: string;
|
||||
mode: "single" | "multi";
|
||||
info: {
|
||||
name: string;
|
||||
verifiedLength: string;
|
||||
verifyIntegrityPending: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default class Aria2 {
|
||||
constructor(options: any);
|
||||
open: () => Promise<void>;
|
||||
call(
|
||||
method: "addUri",
|
||||
uris: string[],
|
||||
options: { dir: string }
|
||||
): Promise<string>;
|
||||
call(
|
||||
method: "tellStatus",
|
||||
gid: string,
|
||||
keys?: string[]
|
||||
): Promise<StatusResponse>;
|
||||
call(method: "pause", gid: string): Promise<string>;
|
||||
call(method: "forcePause", gid: string): Promise<string>;
|
||||
call(method: "unpause", gid: string): Promise<string>;
|
||||
call(method: "remove", gid: string): Promise<string>;
|
||||
call(method: "forceRemove", gid: string): Promise<string>;
|
||||
call(method: "pauseAll"): Promise<string>;
|
||||
call(method: "forcePauseAll"): Promise<string>;
|
||||
listNotifications: () => [
|
||||
"onDownloadStart",
|
||||
"onDownloadPause",
|
||||
"onDownloadStop",
|
||||
"onDownloadComplete",
|
||||
"onDownloadError",
|
||||
"onBtDownloadComplete",
|
||||
];
|
||||
on: (event: string, callback: (params: any) => void) => void;
|
||||
}
|
||||
}
|
@ -10,7 +10,8 @@ import {
|
||||
import { Repack } from "./repack.entity";
|
||||
|
||||
import type { GameShop } from "@types";
|
||||
import { Downloader, GameStatus } from "@shared";
|
||||
import { Downloader } from "@shared";
|
||||
import type { Aria2Status } from "aria2";
|
||||
|
||||
@Entity("game")
|
||||
export class Game {
|
||||
@ -42,7 +43,7 @@ export class Game {
|
||||
shop: GameShop;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
status: GameStatus | null;
|
||||
status: Aria2Status | null;
|
||||
|
||||
@Column("int", { default: Downloader.Torrent })
|
||||
downloader: Downloader;
|
||||
@ -53,9 +54,6 @@ export class Game {
|
||||
@Column("float", { default: 0 })
|
||||
progress: number;
|
||||
|
||||
@Column("float", { default: 0 })
|
||||
fileVerificationProgress: number;
|
||||
|
||||
@Column("int", { default: 0 })
|
||||
bytesDownloaded: number;
|
||||
|
||||
|
@ -10,24 +10,23 @@ const sendEvent = (event: AppUpdaterEvents) => {
|
||||
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
||||
};
|
||||
|
||||
const mockValuesForDebug = async () => {
|
||||
const mockValuesForDebug = () => {
|
||||
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
||||
// sendEvent({ type: "update-downloaded" });
|
||||
};
|
||||
|
||||
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
autoUpdater
|
||||
.addListener("update-available", (info: UpdateInfo) => {
|
||||
.once("update-available", (info: UpdateInfo) => {
|
||||
sendEvent({ type: "update-available", info });
|
||||
})
|
||||
.addListener("update-downloaded", () => {
|
||||
.once("update-downloaded", () => {
|
||||
sendEvent({ type: "update-downloaded" });
|
||||
});
|
||||
|
||||
if (app.isPackaged) {
|
||||
autoUpdater.checkForUpdates();
|
||||
} else {
|
||||
await mockValuesForDebug();
|
||||
mockValuesForDebug();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -29,6 +29,7 @@ import "./user-preferences/update-user-preferences";
|
||||
import "./user-preferences/auto-launch";
|
||||
import "./autoupdater/check-for-updates";
|
||||
import "./autoupdater/restart-and-install-update";
|
||||
import "./user-preferences/authenticate-real-debrid";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => app.getVersion());
|
||||
|
@ -10,7 +10,7 @@ const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectID: string,
|
||||
title: string,
|
||||
gameShop: GameShop,
|
||||
shop: GameShop,
|
||||
executablePath: string | null
|
||||
) => {
|
||||
return gameRepository
|
||||
@ -19,7 +19,7 @@ const addGameToLibrary = async (
|
||||
objectID,
|
||||
},
|
||||
{
|
||||
shop: gameShop,
|
||||
shop,
|
||||
status: null,
|
||||
executablePath,
|
||||
isDeleted: false,
|
||||
@ -40,7 +40,7 @@ const addGameToLibrary = async (
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
shop: gameShop,
|
||||
shop,
|
||||
executablePath,
|
||||
})
|
||||
.then(() => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
import { GameStatus } from "@shared";
|
||||
import { In } from "typeorm";
|
||||
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||
@ -15,7 +16,7 @@ const deleteGameFolder = async (
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
id: gameId,
|
||||
status: GameStatus.Cancelled,
|
||||
status: In(["removed", "complete"]),
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
@ -2,7 +2,6 @@ import { gameRepository } from "@main/repository";
|
||||
|
||||
import { searchRepacks } from "../helpers/search-games";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameStatus } from "@shared";
|
||||
import { sortBy } from "lodash-es";
|
||||
|
||||
const getLibrary = async () =>
|
||||
@ -24,7 +23,7 @@ const getLibrary = async () =>
|
||||
...game,
|
||||
repacks: searchRepacks(game.title),
|
||||
})),
|
||||
(game) => (game.status !== GameStatus.Cancelled ? 0 : 1)
|
||||
(game) => (game.status !== "removed" ? 0 : 1)
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
import { GameStatus } from "@shared";
|
||||
|
||||
const removeGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -9,10 +8,9 @@ const removeGame = async (
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: gameId,
|
||||
status: GameStatus.Cancelled,
|
||||
},
|
||||
{
|
||||
status: null,
|
||||
status: "removed",
|
||||
downloadPath: null,
|
||||
bytesDownloaded: 0,
|
||||
progress: 0,
|
||||
|
@ -1,53 +1,25 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
import { In } from "typeorm";
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { GameStatus } from "@shared";
|
||||
|
||||
const cancelGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
await DownloadManager.cancelDownload(gameId);
|
||||
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: gameId,
|
||||
isDeleted: false,
|
||||
status: In([
|
||||
GameStatus.Downloading,
|
||||
GameStatus.DownloadingMetadata,
|
||||
GameStatus.CheckingFiles,
|
||||
GameStatus.Paused,
|
||||
GameStatus.Seeding,
|
||||
GameStatus.Finished,
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
if (!game) return;
|
||||
DownloadManager.cancelDownload();
|
||||
|
||||
await gameRepository
|
||||
.update(
|
||||
{
|
||||
id: game.id,
|
||||
},
|
||||
{
|
||||
status: GameStatus.Cancelled,
|
||||
bytesDownloaded: 0,
|
||||
progress: 0,
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
if (
|
||||
game.status !== GameStatus.Paused &&
|
||||
game.status !== GameStatus.Seeding
|
||||
) {
|
||||
if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
|
||||
}
|
||||
});
|
||||
{
|
||||
status: "removed",
|
||||
bytesDownloaded: 0,
|
||||
progress: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||
|
@ -1,30 +1,13 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
import { In } from "typeorm";
|
||||
import { DownloadManager, WindowManager } from "@main/services";
|
||||
import { GameStatus } from "@shared";
|
||||
import { DownloadManager } from "@main/services";
|
||||
|
||||
const pauseGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
) => {
|
||||
DownloadManager.pauseDownload();
|
||||
|
||||
await gameRepository
|
||||
.update(
|
||||
{
|
||||
id: gameId,
|
||||
status: In([
|
||||
GameStatus.Downloading,
|
||||
GameStatus.DownloadingMetadata,
|
||||
GameStatus.CheckingFiles,
|
||||
]),
|
||||
},
|
||||
{ status: GameStatus.Paused }
|
||||
)
|
||||
.then((result) => {
|
||||
if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
|
||||
});
|
||||
await DownloadManager.pauseDownload();
|
||||
await gameRepository.update({ id: gameId }, { status: "paused" });
|
||||
};
|
||||
|
||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Not } from "typeorm";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||
import { In } from "typeorm";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { GameStatus } from "@shared";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { Game } from "@main/entity";
|
||||
|
||||
const resumeGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -18,31 +20,21 @@ const resumeGameDownload = async (
|
||||
});
|
||||
|
||||
if (!game) return;
|
||||
DownloadManager.pauseDownload();
|
||||
|
||||
if (game.status === GameStatus.Paused) {
|
||||
const downloadsPath = game.downloadPath ?? (await getDownloadsPath());
|
||||
if (game.status === "paused") {
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
DownloadManager.resumeDownload(gameId);
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ status: "active", progress: Not(1) }, { status: "paused" });
|
||||
|
||||
await gameRepository.update(
|
||||
{
|
||||
status: In([
|
||||
GameStatus.Downloading,
|
||||
GameStatus.DownloadingMetadata,
|
||||
GameStatus.CheckingFiles,
|
||||
]),
|
||||
},
|
||||
{ status: GameStatus.Paused }
|
||||
);
|
||||
await DownloadManager.resumeDownload(game);
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: game.id },
|
||||
{
|
||||
status: GameStatus.Downloading,
|
||||
downloadPath: downloadsPath,
|
||||
}
|
||||
);
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ id: gameId }, { status: "active" });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,39 +1,26 @@
|
||||
import {
|
||||
gameRepository,
|
||||
repackRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { gameRepository, repackRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { GameShop } from "@types";
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||
import { In } from "typeorm";
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { Downloader, GameStatus } from "@shared";
|
||||
import { stateManager } from "@main/state-manager";
|
||||
import { Not } from "typeorm";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
repackId: number,
|
||||
objectID: string,
|
||||
title: string,
|
||||
gameShop: GameShop,
|
||||
downloadPath: string
|
||||
payload: StartGameDownloadPayload
|
||||
) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const downloader = userPreferences?.realDebridApiToken
|
||||
? Downloader.RealDebrid
|
||||
: Downloader.Torrent;
|
||||
const { repackId, objectID, title, shop, downloadPath, downloader } = payload;
|
||||
|
||||
const [game, repack] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
shop,
|
||||
},
|
||||
relations: { repack: true },
|
||||
}),
|
||||
repackRepository.findOne({
|
||||
where: {
|
||||
@ -42,18 +29,13 @@ const startGameDownload = async (
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!repack || game?.status === GameStatus.Downloading) return;
|
||||
DownloadManager.pauseDownload();
|
||||
if (!repack) return;
|
||||
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await gameRepository.update(
|
||||
{
|
||||
status: In([
|
||||
GameStatus.Downloading,
|
||||
GameStatus.DownloadingMetadata,
|
||||
GameStatus.CheckingFiles,
|
||||
]),
|
||||
},
|
||||
{ status: GameStatus.Paused }
|
||||
{ status: "active", progress: Not(1) },
|
||||
{ status: "paused" }
|
||||
);
|
||||
|
||||
if (game) {
|
||||
@ -62,19 +44,15 @@ const startGameDownload = async (
|
||||
id: game.id,
|
||||
},
|
||||
{
|
||||
status: GameStatus.DownloadingMetadata,
|
||||
downloadPath: downloadPath,
|
||||
status: "active",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
repack: { id: repackId },
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
|
||||
DownloadManager.downloadGame(game.id);
|
||||
|
||||
game.status = GameStatus.DownloadingMetadata;
|
||||
|
||||
return game;
|
||||
} else {
|
||||
const steamGame = stateManager
|
||||
.getValue("steamGames")
|
||||
@ -84,14 +62,14 @@ const startGameDownload = async (
|
||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
const createdGame = await gameRepository
|
||||
.save({
|
||||
await gameRepository
|
||||
.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
downloader,
|
||||
shop: gameShop,
|
||||
status: GameStatus.Downloading,
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
repack: { id: repackId },
|
||||
})
|
||||
@ -104,13 +82,16 @@ const startGameDownload = async (
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
DownloadManager.downloadGame(createdGame.id);
|
||||
|
||||
const { repack: _, ...rest } = createdGame;
|
||||
|
||||
return rest;
|
||||
}
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
},
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
};
|
||||
|
||||
registerEvent("startGameDownload", startGameDownload);
|
||||
|
14
src/main/events/user-preferences/authenticate-real-debrid.ts
Normal file
14
src/main/events/user-preferences/authenticate-real-debrid.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { RealDebridClient } from "@main/services/real-debrid";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const authenticateRealDebrid = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
apiToken: string
|
||||
) => {
|
||||
RealDebridClient.authorize(apiToken);
|
||||
|
||||
const user = await RealDebridClient.getUser();
|
||||
return user;
|
||||
};
|
||||
|
||||
registerEvent("authenticateRealDebrid", authenticateRealDebrid);
|
@ -2,23 +2,17 @@ import { userPreferencesRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { UserPreferences } from "@types";
|
||||
import { RealDebridClient } from "@main/services/real-debrid";
|
||||
|
||||
const updateUserPreferences = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
preferences: Partial<UserPreferences>
|
||||
) => {
|
||||
if (preferences.realDebridApiToken) {
|
||||
RealDebridClient.authorize(preferences.realDebridApiToken);
|
||||
}
|
||||
|
||||
await userPreferencesRepository.upsert(
|
||||
) =>
|
||||
userPreferencesRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
...preferences,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("updateUserPreferences", updateUserPreferences);
|
||||
|
@ -85,5 +85,8 @@ export const steamUrlBuilder = {
|
||||
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
|
||||
};
|
||||
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export * from "./formatters";
|
||||
export * from "./ps";
|
||||
|
@ -3,7 +3,12 @@ import updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, resolveDatabaseUpdates, WindowManager } from "@main/services";
|
||||
import {
|
||||
DownloadManager,
|
||||
logger,
|
||||
resolveDatabaseUpdates,
|
||||
WindowManager,
|
||||
} from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import * as resources from "@locales";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
@ -100,6 +105,10 @@ app.on("window-all-closed", () => {
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
DownloadManager.disconnect();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
|
@ -5,27 +5,25 @@ import {
|
||||
getNewRepacksFromUser,
|
||||
getNewRepacksFromXatab,
|
||||
getNewRepacksFromOnlineFix,
|
||||
startProcessWatcher,
|
||||
DownloadManager,
|
||||
startMainLoop,
|
||||
} from "./services";
|
||||
import {
|
||||
gameRepository,
|
||||
repackRepository,
|
||||
userPreferencesRepository,
|
||||
} from "./repository";
|
||||
import { TorrentDownloader } from "./services";
|
||||
import { Repack, UserPreferences } from "./entity";
|
||||
import { Notification } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { GameStatus } from "@shared";
|
||||
import { In } from "typeorm";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { RealDebridClient } from "./services/real-debrid";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { SteamGame } from "@types";
|
||||
import { Not } from "typeorm";
|
||||
|
||||
startProcessWatcher();
|
||||
startMainLoop();
|
||||
|
||||
const track1337xUsers = async (existingRepacks: Repack[]) => {
|
||||
for (const repacker of repackersOn1337x) {
|
||||
@ -72,7 +70,7 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
|
||||
};
|
||||
|
||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
const repacks = await repackRepository.find({
|
||||
const repacks = repackRepository.find({
|
||||
order: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
@ -82,31 +80,24 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
|
||||
) as SteamGame[];
|
||||
|
||||
stateManager.setValue("repacks", repacks);
|
||||
stateManager.setValue("repacks", await repacks);
|
||||
stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc"));
|
||||
|
||||
import("./events");
|
||||
|
||||
if (userPreferences?.realDebridApiToken)
|
||||
await RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
status: In([
|
||||
GameStatus.Downloading,
|
||||
GameStatus.DownloadingMetadata,
|
||||
GameStatus.CheckingFiles,
|
||||
]),
|
||||
status: "active",
|
||||
progress: Not(1),
|
||||
isDeleted: false,
|
||||
},
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
await TorrentDownloader.startClient();
|
||||
|
||||
if (game) {
|
||||
DownloadManager.resumeDownload(game.id);
|
||||
}
|
||||
if (game) DownloadManager.startDownload(game);
|
||||
};
|
||||
|
||||
userPreferencesRepository
|
||||
|
20
src/main/services/aria2c.ts
Normal file
20
src/main/services/aria2c.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { app } from "electron";
|
||||
|
||||
export const startAria2 = () => {
|
||||
const binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
|
||||
return spawn(
|
||||
binaryPath,
|
||||
[
|
||||
"--enable-rpc",
|
||||
"--rpc-listen-all",
|
||||
"--file-allocation=none",
|
||||
"--allow-overwrite=true",
|
||||
],
|
||||
{ stdio: "inherit", windowsHide: true }
|
||||
);
|
||||
};
|
@ -1,76 +1,303 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import Aria2, { StatusResponse } from "aria2";
|
||||
|
||||
import type { Game } from "@main/entity";
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { RealDebridClient } from "./real-debrid";
|
||||
import { Notification } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
import { writePipe } from "./fifo";
|
||||
import { RealDebridDownloader } from "./downloaders";
|
||||
import { DownloadProgress } from "@types";
|
||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
import { Game } from "@main/entity";
|
||||
import { startAria2 } from "./aria2c";
|
||||
import { sleep } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
|
||||
export class DownloadManager {
|
||||
private static gameDownloading: Game;
|
||||
private static downloads = new Map<number, string>();
|
||||
|
||||
static async getGame(gameId: number) {
|
||||
return gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
relations: {
|
||||
repack: true,
|
||||
},
|
||||
});
|
||||
private static connected = false;
|
||||
private static gid: string | null = null;
|
||||
private static game: Game | null = null;
|
||||
private static realDebridTorrentId: string | null = null;
|
||||
private static aria2c: ChildProcess | null = null;
|
||||
|
||||
private static aria2 = new Aria2({});
|
||||
|
||||
private static async connect() {
|
||||
this.aria2c = startAria2();
|
||||
|
||||
let retries = 0;
|
||||
|
||||
while (retries < 4 && !this.connected) {
|
||||
try {
|
||||
await this.aria2.open();
|
||||
logger.log("Connected to aria2");
|
||||
|
||||
this.connected = true;
|
||||
} catch (err) {
|
||||
await sleep(100);
|
||||
logger.log("Failed to connect to aria2, retrying...");
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async cancelDownload() {
|
||||
if (
|
||||
this.gameDownloading &&
|
||||
this.gameDownloading.downloader === Downloader.Torrent
|
||||
) {
|
||||
writePipe.write({ action: "cancel" });
|
||||
} else {
|
||||
RealDebridDownloader.destroy();
|
||||
public static disconnect() {
|
||||
if (this.aria2c) {
|
||||
this.aria2c.kill();
|
||||
}
|
||||
}
|
||||
|
||||
private static getETA(
|
||||
totalLength: number,
|
||||
completedLength: number,
|
||||
speed: number
|
||||
) {
|
||||
const remainingBytes = totalLength - completedLength;
|
||||
|
||||
if (remainingBytes >= 0 && speed > 0) {
|
||||
return (remainingBytes / speed) * 1000;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static async publishNotification() {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled && this.game) {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
}),
|
||||
body: t("game_ready_to_install", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
title: this.game.title,
|
||||
}),
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static getFolderName(status: StatusResponse) {
|
||||
if (status.bittorrent?.info) return status.bittorrent.info.name;
|
||||
return "";
|
||||
}
|
||||
|
||||
private static async getRealDebridDownloadUrl() {
|
||||
if (this.realDebridTorrentId) {
|
||||
const torrentInfo = await RealDebridClient.getTorrentInfo(
|
||||
this.realDebridTorrentId
|
||||
);
|
||||
|
||||
const { status, links } = torrentInfo;
|
||||
|
||||
if (status === "waiting_files_selection") {
|
||||
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === "downloaded") {
|
||||
const [link] = links;
|
||||
const { download } = await RealDebridClient.unrestrictLink(link);
|
||||
return decodeURIComponent(download);
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
const progress = torrentInfo.progress / 100;
|
||||
const totalDownloaded = progress * torrentInfo.bytes;
|
||||
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
|
||||
const payload = {
|
||||
numPeers: 0,
|
||||
numSeeds: torrentInfo.seeders,
|
||||
downloadSpeed: torrentInfo.speed,
|
||||
timeRemaining: this.getETA(
|
||||
torrentInfo.bytes,
|
||||
totalDownloaded,
|
||||
torrentInfo.speed
|
||||
),
|
||||
isDownloadingMetadata: status === "magnet_conversion",
|
||||
game: {
|
||||
...this.game,
|
||||
bytesDownloaded: progress * torrentInfo.bytes,
|
||||
progress,
|
||||
},
|
||||
} as DownloadProgress;
|
||||
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-download-progress",
|
||||
JSON.parse(JSON.stringify(payload))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async watchDownloads() {
|
||||
if (!this.game) return;
|
||||
|
||||
if (!this.gid && this.realDebridTorrentId) {
|
||||
const options = { dir: this.game.downloadPath! };
|
||||
const downloadUrl = await this.getRealDebridDownloadUrl();
|
||||
|
||||
if (downloadUrl) {
|
||||
this.gid = await this.aria2.call("addUri", [downloadUrl], options);
|
||||
this.downloads.set(this.game.id, this.gid);
|
||||
this.realDebridTorrentId = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.gid) return;
|
||||
|
||||
const status = await this.aria2.call("tellStatus", this.gid);
|
||||
|
||||
const isDownloadingMetadata = status.bittorrent && !status.bittorrent?.info;
|
||||
|
||||
if (status.followedBy?.length) {
|
||||
this.gid = status.followedBy[0];
|
||||
this.downloads.set(this.game.id, this.gid);
|
||||
return;
|
||||
}
|
||||
|
||||
const progress =
|
||||
Number(status.completedLength) / Number(status.totalLength);
|
||||
|
||||
if (!isDownloadingMetadata) {
|
||||
const update: QueryDeepPartialEntity<Game> = {
|
||||
bytesDownloaded: Number(status.completedLength),
|
||||
fileSize: Number(status.totalLength),
|
||||
status: status.status,
|
||||
};
|
||||
|
||||
if (!isNaN(progress)) update.progress = progress;
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: this.game.id },
|
||||
{
|
||||
...update,
|
||||
status: status.status,
|
||||
folderName: this.getFolderName(status),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: this.game.id, isDeleted: false },
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
||||
await this.publishNotification();
|
||||
|
||||
/*
|
||||
Only cancel bittorrent downloads to stop seeding
|
||||
*/
|
||||
if (status.bittorrent) {
|
||||
await this.cancelDownload(this.game.id);
|
||||
} else {
|
||||
this.clearCurrentDownload();
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow && game) {
|
||||
if (!isNaN(progress))
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
|
||||
const payload = {
|
||||
numPeers: Number(status.connections),
|
||||
numSeeds: Number(status.numSeeders ?? 0),
|
||||
downloadSpeed: Number(status.downloadSpeed),
|
||||
timeRemaining: this.getETA(
|
||||
Number(status.totalLength),
|
||||
Number(status.completedLength),
|
||||
Number(status.downloadSpeed)
|
||||
),
|
||||
isDownloadingMetadata: !!isDownloadingMetadata,
|
||||
game,
|
||||
} as DownloadProgress;
|
||||
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-download-progress",
|
||||
JSON.parse(JSON.stringify(payload))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static clearCurrentDownload() {
|
||||
if (this.game) {
|
||||
this.downloads.delete(this.game.id);
|
||||
this.gid = null;
|
||||
this.game = null;
|
||||
this.realDebridTorrentId = null;
|
||||
}
|
||||
}
|
||||
|
||||
static async cancelDownload(gameId: number) {
|
||||
const gid = this.downloads.get(gameId);
|
||||
|
||||
if (gid) {
|
||||
await this.aria2.call("remove", gid);
|
||||
|
||||
if (this.gid === gid) {
|
||||
this.clearCurrentDownload();
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
} else {
|
||||
this.downloads.delete(gameId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async pauseDownload() {
|
||||
if (
|
||||
this.gameDownloading &&
|
||||
this.gameDownloading.downloader === Downloader.Torrent
|
||||
) {
|
||||
writePipe.write({ action: "pause" });
|
||||
if (this.gid) {
|
||||
await this.aria2.call("forcePause", this.gid);
|
||||
this.gid = null;
|
||||
}
|
||||
|
||||
this.game = null;
|
||||
this.realDebridTorrentId = null;
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
}
|
||||
|
||||
static async resumeDownload(game: Game) {
|
||||
if (this.downloads.has(game.id)) {
|
||||
const gid = this.downloads.get(game.id)!;
|
||||
await this.aria2.call("unpause", gid);
|
||||
|
||||
this.gid = gid;
|
||||
this.game = game;
|
||||
this.realDebridTorrentId = null;
|
||||
} else {
|
||||
RealDebridDownloader.destroy();
|
||||
return this.startDownload(game);
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeDownload(gameId: number) {
|
||||
const game = await this.getGame(gameId);
|
||||
static async startDownload(game: Game) {
|
||||
if (!this.connected) await this.connect();
|
||||
|
||||
if (game!.downloader === Downloader.Torrent) {
|
||||
writePipe.write({
|
||||
action: "start",
|
||||
game_id: game!.id,
|
||||
magnet: game!.repack.magnet,
|
||||
save_path: game!.downloadPath,
|
||||
});
|
||||
const options = {
|
||||
dir: game.downloadPath!,
|
||||
};
|
||||
|
||||
if (game.downloader === Downloader.RealDebrid) {
|
||||
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
|
||||
game!.repack.magnet
|
||||
);
|
||||
} else {
|
||||
RealDebridDownloader.startDownload(game!);
|
||||
this.gid = await this.aria2.call("addUri", [game.repack.magnet], options);
|
||||
this.downloads.set(game.id, this.gid);
|
||||
}
|
||||
|
||||
this.gameDownloading = game!;
|
||||
}
|
||||
|
||||
static async downloadGame(gameId: number) {
|
||||
const game = await this.getGame(gameId);
|
||||
|
||||
if (game!.downloader === Downloader.Torrent) {
|
||||
writePipe.write({
|
||||
action: "start",
|
||||
game_id: game!.id,
|
||||
magnet: game!.repack.magnet,
|
||||
save_path: game!.downloadPath,
|
||||
});
|
||||
} else {
|
||||
RealDebridDownloader.startDownload(game!);
|
||||
}
|
||||
|
||||
this.gameDownloading = game!;
|
||||
this.game = game;
|
||||
}
|
||||
}
|
||||
|
@ -1,85 +0,0 @@
|
||||
import { t } from "i18next";
|
||||
import { Notification } from "electron";
|
||||
|
||||
import { Game } from "@main/entity";
|
||||
|
||||
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
|
||||
import { WindowManager } from "../window-manager";
|
||||
import type { TorrentUpdate } from "./torrent.downloader";
|
||||
|
||||
import { GameStatus } from "@shared";
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
|
||||
interface DownloadStatus {
|
||||
numPeers?: number;
|
||||
numSeeds?: number;
|
||||
downloadSpeed?: number;
|
||||
timeRemaining?: number;
|
||||
}
|
||||
|
||||
export class Downloader {
|
||||
static getGameProgress(game: Game) {
|
||||
if (game.status === GameStatus.CheckingFiles)
|
||||
return game.fileVerificationProgress;
|
||||
|
||||
return game.progress;
|
||||
}
|
||||
|
||||
static async updateGameProgress(
|
||||
gameId: number,
|
||||
gameUpdate: QueryDeepPartialEntity<Game>,
|
||||
downloadStatus: DownloadStatus
|
||||
) {
|
||||
await gameRepository.update({ id: gameId }, gameUpdate);
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
if (game?.progress === 1) {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled) {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
}),
|
||||
body: t("game_ready_to_install", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
title: game?.title,
|
||||
}),
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow && game) {
|
||||
const progress = this.getGameProgress(game);
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-download-progress",
|
||||
JSON.parse(
|
||||
JSON.stringify({
|
||||
...({
|
||||
progress: gameUpdate.progress,
|
||||
bytesDownloaded: gameUpdate.bytesDownloaded,
|
||||
fileSize: gameUpdate.fileSize,
|
||||
gameId,
|
||||
numPeers: downloadStatus.numPeers,
|
||||
numSeeds: downloadStatus.numSeeds,
|
||||
downloadSpeed: downloadStatus.downloadSpeed,
|
||||
timeRemaining: downloadStatus.timeRemaining,
|
||||
} as TorrentUpdate),
|
||||
game,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./real-debrid.downloader";
|
||||
export * from "./torrent.downloader";
|
@ -1,115 +0,0 @@
|
||||
import { Game } from "@main/entity";
|
||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import EasyDL from "easydl";
|
||||
import { GameStatus } from "@shared";
|
||||
// import { fullArchive } from "node-7z-archive";
|
||||
|
||||
import { Downloader } from "./downloader";
|
||||
import { RealDebridClient } from "../real-debrid";
|
||||
|
||||
export class RealDebridDownloader extends Downloader {
|
||||
private static download: EasyDL;
|
||||
private static downloadSize = 0;
|
||||
|
||||
private static getEta(bytesDownloaded: number, speed: number) {
|
||||
const remainingBytes = this.downloadSize - bytesDownloaded;
|
||||
|
||||
if (remainingBytes >= 0 && speed > 0) {
|
||||
return (remainingBytes / speed) * 1000;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static createFolderIfNotExists(path: string) {
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdirSync(path);
|
||||
}
|
||||
}
|
||||
|
||||
// private static async startDecompression(
|
||||
// rarFile: string,
|
||||
// dest: string,
|
||||
// game: Game
|
||||
// ) {
|
||||
// await fullArchive(rarFile, dest);
|
||||
|
||||
// const updatePayload: QueryDeepPartialEntity<Game> = {
|
||||
// status: GameStatus.Finished,
|
||||
// };
|
||||
|
||||
// await this.updateGameProgress(game.id, updatePayload, {});
|
||||
// }
|
||||
|
||||
static destroy() {
|
||||
if (this.download) {
|
||||
this.download.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
static async startDownload(game: Game) {
|
||||
if (this.download) this.download.destroy();
|
||||
const downloadUrl = decodeURIComponent(
|
||||
await RealDebridClient.getDownloadUrl(game)
|
||||
);
|
||||
|
||||
const filename = path.basename(downloadUrl);
|
||||
const folderName = path.basename(filename, path.extname(filename));
|
||||
|
||||
const downloadPath = path.join(game.downloadPath!, folderName);
|
||||
this.createFolderIfNotExists(downloadPath);
|
||||
|
||||
this.download = new EasyDL(downloadUrl, path.join(downloadPath, filename));
|
||||
|
||||
const metadata = await this.download.metadata();
|
||||
|
||||
this.downloadSize = metadata.size;
|
||||
|
||||
const updatePayload: QueryDeepPartialEntity<Game> = {
|
||||
status: GameStatus.Downloading,
|
||||
fileSize: metadata.size,
|
||||
folderName,
|
||||
};
|
||||
|
||||
const downloadStatus = {
|
||||
timeRemaining: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
|
||||
await this.updateGameProgress(game.id, updatePayload, downloadStatus);
|
||||
|
||||
this.download.on("progress", async ({ total }) => {
|
||||
const updatePayload: QueryDeepPartialEntity<Game> = {
|
||||
status: GameStatus.Downloading,
|
||||
progress: Math.min(0.99, total.percentage / 100),
|
||||
bytesDownloaded: total.bytes,
|
||||
};
|
||||
|
||||
const downloadStatus = {
|
||||
downloadSpeed: total.speed,
|
||||
timeRemaining: this.getEta(total.bytes ?? 0, total.speed ?? 0),
|
||||
};
|
||||
|
||||
await this.updateGameProgress(game.id, updatePayload, downloadStatus);
|
||||
});
|
||||
|
||||
this.download.on("end", async () => {
|
||||
const updatePayload: QueryDeepPartialEntity<Game> = {
|
||||
status: GameStatus.Finished,
|
||||
progress: 1,
|
||||
};
|
||||
|
||||
await this.updateGameProgress(game.id, updatePayload, {
|
||||
timeRemaining: 0,
|
||||
});
|
||||
|
||||
/* This has to be improved */
|
||||
// this.startDecompression(
|
||||
// path.join(downloadPath, filename),
|
||||
// downloadPath,
|
||||
// game
|
||||
// );
|
||||
});
|
||||
}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
import path from "node:path";
|
||||
import cp from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { app, dialog } from "electron";
|
||||
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
|
||||
import { Game } from "@main/entity";
|
||||
import { GameStatus } from "@shared";
|
||||
import { Downloader } from "./downloader";
|
||||
import { readPipe, writePipe } from "../fifo";
|
||||
|
||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||
darwin: "hydra-download-manager",
|
||||
linux: "hydra-download-manager",
|
||||
win32: "hydra-download-manager.exe",
|
||||
};
|
||||
|
||||
enum TorrentState {
|
||||
CheckingFiles = 1,
|
||||
DownloadingMetadata = 2,
|
||||
Downloading = 3,
|
||||
Finished = 4,
|
||||
Seeding = 5,
|
||||
}
|
||||
|
||||
export interface TorrentUpdate {
|
||||
gameId: number;
|
||||
progress: number;
|
||||
downloadSpeed: number;
|
||||
timeRemaining: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
status: TorrentState;
|
||||
folderName: string;
|
||||
fileSize: number;
|
||||
bytesDownloaded: number;
|
||||
}
|
||||
|
||||
export const BITTORRENT_PORT = "5881";
|
||||
|
||||
export class TorrentDownloader extends Downloader {
|
||||
private static messageLength = 1024 * 2;
|
||||
|
||||
public static async attachListener() {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const buffer = readPipe.socket?.read(this.messageLength);
|
||||
|
||||
if (buffer === null) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = Buffer.from(
|
||||
buffer.slice(0, buffer.indexOf(0x00))
|
||||
).toString("utf-8");
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(message) as TorrentUpdate;
|
||||
|
||||
const updatePayload: QueryDeepPartialEntity<Game> = {
|
||||
bytesDownloaded: payload.bytesDownloaded,
|
||||
status: this.getTorrentStateName(payload.status),
|
||||
};
|
||||
|
||||
if (payload.status === TorrentState.CheckingFiles) {
|
||||
updatePayload.fileVerificationProgress = payload.progress;
|
||||
} else {
|
||||
if (payload.folderName) {
|
||||
updatePayload.folderName = payload.folderName;
|
||||
updatePayload.fileSize = payload.fileSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
[TorrentState.Downloading, TorrentState.Seeding].includes(
|
||||
payload.status
|
||||
)
|
||||
) {
|
||||
updatePayload.progress = payload.progress;
|
||||
}
|
||||
|
||||
this.updateGameProgress(payload.gameId, updatePayload, {
|
||||
numPeers: payload.numPeers,
|
||||
numSeeds: payload.numSeeds,
|
||||
downloadSpeed: payload.downloadSpeed,
|
||||
timeRemaining: payload.timeRemaining,
|
||||
});
|
||||
} finally {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static startClient() {
|
||||
return new Promise((resolve) => {
|
||||
const commonArgs = [
|
||||
BITTORRENT_PORT,
|
||||
writePipe.socketPath,
|
||||
readPipe.socketPath,
|
||||
];
|
||||
|
||||
if (app.isPackaged) {
|
||||
const binaryName = binaryNameByPlatform[process.platform]!;
|
||||
const binaryPath = path.join(
|
||||
process.resourcesPath,
|
||||
"hydra-download-manager",
|
||||
binaryName
|
||||
);
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
dialog.showErrorBox(
|
||||
"Fatal",
|
||||
"Hydra download manager binary not found. Please check if it has been removed by Windows Defender."
|
||||
);
|
||||
|
||||
app.quit();
|
||||
}
|
||||
|
||||
cp.spawn(binaryPath, commonArgs, {
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
} else {
|
||||
const scriptPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"torrent-client",
|
||||
"main.py"
|
||||
);
|
||||
|
||||
cp.spawn("python3", [scriptPath, ...commonArgs], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(
|
||||
async () => {
|
||||
this.attachListener();
|
||||
resolve(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private static getTorrentStateName(state: TorrentState) {
|
||||
if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles;
|
||||
if (state === TorrentState.Downloading) return GameStatus.Downloading;
|
||||
if (state === TorrentState.DownloadingMetadata)
|
||||
return GameStatus.DownloadingMetadata;
|
||||
if (state === TorrentState.Finished) return GameStatus.Finished;
|
||||
if (state === TorrentState.Seeding) return GameStatus.Seeding;
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import path from "node:path";
|
||||
import net from "node:net";
|
||||
import crypto from "node:crypto";
|
||||
import os from "node:os";
|
||||
|
||||
export class FIFO {
|
||||
public socket: null | net.Socket = null;
|
||||
public socketPath = this.generateSocketFilename();
|
||||
|
||||
private generateSocketFilename() {
|
||||
const hash = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
if (process.platform === "win32") {
|
||||
return "\\\\.\\pipe\\" + hash;
|
||||
}
|
||||
|
||||
return path.join(os.tmpdir(), hash);
|
||||
}
|
||||
|
||||
public write(data: any) {
|
||||
if (!this.socket) return;
|
||||
this.socket.write(Buffer.from(JSON.stringify(data)));
|
||||
}
|
||||
|
||||
public createPipe() {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer((socket) => {
|
||||
this.socket = socket;
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
server.listen(this.socketPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const writePipe = new FIFO();
|
||||
export const readPipe = new FIFO();
|
@ -5,8 +5,7 @@ export * from "./steam-250";
|
||||
export * from "./steam-grid";
|
||||
export * from "./update-resolver";
|
||||
export * from "./window-manager";
|
||||
export * from "./fifo";
|
||||
export * from "./downloaders";
|
||||
export * from "./download-manager";
|
||||
export * from "./how-long-to-beat";
|
||||
export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
|
15
src/main/services/main-loop.ts
Normal file
15
src/main/services/main-loop.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { sleep } from "@main/helpers";
|
||||
import { DownloadManager } from "./download-manager";
|
||||
import { watchProcesses } from "./process-watcher";
|
||||
|
||||
export const startMainLoop = async () => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await Promise.allSettled([
|
||||
watchProcesses(),
|
||||
DownloadManager.watchDownloads(),
|
||||
]);
|
||||
|
||||
await sleep(500);
|
||||
}
|
||||
};
|
@ -5,70 +5,58 @@ import { gameRepository } from "@main/repository";
|
||||
import { getProcesses } from "@main/helpers";
|
||||
import { WindowManager } from "./window-manager";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const gamesPlaytime = new Map<number, number>();
|
||||
|
||||
export const startProcessWatcher = async () => {
|
||||
const sleepTime = 500;
|
||||
const gamesPlaytime = new Map<number, number>();
|
||||
export const watchProcesses = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
executablePath: Not(IsNull()),
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
executablePath: Not(IsNull()),
|
||||
isDeleted: false,
|
||||
},
|
||||
if (games.length === 0) return;
|
||||
|
||||
const processes = await getProcesses();
|
||||
|
||||
for (const game of games) {
|
||||
const executablePath = game.executablePath!;
|
||||
const basename = path.win32.basename(executablePath);
|
||||
const basenameWithoutExtension = path.win32.basename(
|
||||
executablePath,
|
||||
path.extname(executablePath)
|
||||
);
|
||||
|
||||
const gameProcess = processes.find((runningProcess) => {
|
||||
if (process.platform === "win32") {
|
||||
return runningProcess.name === basename;
|
||||
}
|
||||
|
||||
return [basename, basenameWithoutExtension].includes(runningProcess.name);
|
||||
});
|
||||
|
||||
if (games.length === 0) {
|
||||
await sleep(sleepTime);
|
||||
continue;
|
||||
}
|
||||
if (gameProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
const zero = gamesPlaytime.get(game.id) ?? 0;
|
||||
const delta = performance.now() - zero;
|
||||
|
||||
const processes = await getProcesses();
|
||||
|
||||
for (const game of games) {
|
||||
const executablePath = game.executablePath!;
|
||||
const basename = path.win32.basename(executablePath);
|
||||
const basenameWithoutExtension = path.win32.basename(
|
||||
executablePath,
|
||||
path.extname(executablePath)
|
||||
);
|
||||
|
||||
const gameProcess = processes.find((runningProcess) => {
|
||||
if (process.platform === "win32") {
|
||||
return runningProcess.name === basename;
|
||||
}
|
||||
|
||||
return [basename, basenameWithoutExtension].includes(
|
||||
runningProcess.name
|
||||
);
|
||||
});
|
||||
|
||||
if (gameProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
const zero = gamesPlaytime.get(game.id) ?? 0;
|
||||
const delta = performance.now() - zero;
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
|
||||
}
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
gamesPlaytime.set(game.id, performance.now());
|
||||
} else if (gamesPlaytime.has(game.id)) {
|
||||
gamesPlaytime.delete(game.id);
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
|
||||
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
|
||||
}
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
gamesPlaytime.set(game.id, performance.now());
|
||||
} else if (gamesPlaytime.has(game.id)) {
|
||||
gamesPlaytime.delete(game.id);
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(sleepTime);
|
||||
}
|
||||
};
|
||||
|
@ -1,15 +1,24 @@
|
||||
import { Game } from "@main/entity";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import parseTorrent from "parse-torrent";
|
||||
import type {
|
||||
RealDebridAddMagnet,
|
||||
RealDebridTorrentInfo,
|
||||
RealDebridUnrestrictLink,
|
||||
} from "./real-debrid.types";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
|
||||
const base = "https://api.real-debrid.com/rest/1.0";
|
||||
RealDebridUser,
|
||||
} from "@types";
|
||||
|
||||
export class RealDebridClient {
|
||||
private static instance: AxiosInstance;
|
||||
private static baseURL = "https://api.real-debrid.com/rest/1.0";
|
||||
|
||||
static authorize(apiToken: string) {
|
||||
this.instance = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async addMagnet(magnet: string) {
|
||||
const searchParams = new URLSearchParams({ magnet });
|
||||
@ -22,13 +31,18 @@ export class RealDebridClient {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async getInfo(id: string) {
|
||||
static async getTorrentInfo(id: string) {
|
||||
const response = await this.instance.get<RealDebridTorrentInfo>(
|
||||
`/torrents/info/${id}`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async getUser() {
|
||||
const response = await this.instance.get<RealDebridUser>(`/user`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async selectAllFiles(id: string) {
|
||||
const searchParams = new URLSearchParams({ files: "all" });
|
||||
|
||||
@ -49,51 +63,24 @@ export class RealDebridClient {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async getAllTorrentsFromUser() {
|
||||
private static async getAllTorrentsFromUser() {
|
||||
const response =
|
||||
await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static extractSHA1FromMagnet(magnet: string) {
|
||||
return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase();
|
||||
}
|
||||
static async getTorrentId(magnetUri: string) {
|
||||
const userTorrents = await RealDebridClient.getAllTorrentsFromUser();
|
||||
|
||||
static async getDownloadUrl(game: Game) {
|
||||
const torrents = await RealDebridClient.getAllTorrentsFromUser();
|
||||
const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet);
|
||||
let torrent = torrents.find((t) => t.hash === hash);
|
||||
const { infoHash } = await parseTorrent(magnetUri);
|
||||
const userTorrent = userTorrents.find(
|
||||
(userTorrent) => userTorrent.hash === infoHash
|
||||
);
|
||||
|
||||
if (!torrent) {
|
||||
const magnet = await RealDebridClient.addMagnet(game!.repack.magnet);
|
||||
if (userTorrent) return userTorrent.id;
|
||||
|
||||
if (magnet && magnet.id) {
|
||||
await RealDebridClient.selectAllFiles(magnet.id);
|
||||
torrent = await RealDebridClient.getInfo(magnet.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (torrent) {
|
||||
const { links } = torrent;
|
||||
const { download } = await RealDebridClient.unrestrictLink(links[0]);
|
||||
|
||||
if (!download) {
|
||||
throw new Error("Torrent not cached on Real Debrid");
|
||||
}
|
||||
|
||||
return download;
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
static async authorize(apiToken: string) {
|
||||
this.instance = axios.create({
|
||||
baseURL: base,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
},
|
||||
});
|
||||
const torrent = await RealDebridClient.addMagnet(magnetUri);
|
||||
return torrent.id;
|
||||
}
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
export interface RealDebridUnrestrictLink {
|
||||
id: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
filesize: number;
|
||||
link: string;
|
||||
host: string;
|
||||
host_icon: string;
|
||||
chunks: number;
|
||||
crc: number;
|
||||
download: string;
|
||||
streamable: number;
|
||||
}
|
||||
|
||||
export interface RealDebridAddMagnet {
|
||||
id: string;
|
||||
// URL of the created ressource
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export interface RealDebridTorrentInfo {
|
||||
id: string;
|
||||
filename: string;
|
||||
original_filename: string; // Original name of the torrent
|
||||
hash: string; // SHA1 Hash of the torrent
|
||||
bytes: number; // Size of selected files only
|
||||
original_bytes: number; // Total size of the torrent
|
||||
host: string; // Host main domain
|
||||
split: number; // Split size of links
|
||||
progress: number; // Possible values: 0 to 100
|
||||
status: string; // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead
|
||||
added: string; // jsonDate
|
||||
files: [
|
||||
{
|
||||
id: number;
|
||||
path: string; // Path to the file inside the torrent, starting with "/"
|
||||
bytes: number;
|
||||
selected: number; // 0 or 1
|
||||
},
|
||||
{
|
||||
id: number;
|
||||
path: string; // Path to the file inside the torrent, starting with "/"
|
||||
bytes: number;
|
||||
selected: number; // 0 or 1
|
||||
},
|
||||
];
|
||||
links: string[];
|
||||
ended: string; // !! Only present when finished, jsonDate
|
||||
speed: number; // !! Only present in "downloading", "compressing", "uploading" status
|
||||
seeders: number; // !! Only present in "downloading", "magnet_conversion" status
|
||||
}
|
@ -5,38 +5,26 @@ import { contextBridge, ipcRenderer } from "electron";
|
||||
import type {
|
||||
CatalogueCategory,
|
||||
GameShop,
|
||||
TorrentProgress,
|
||||
DownloadProgress,
|
||||
UserPreferences,
|
||||
AppUpdaterEvents,
|
||||
AppUpdaterEvent,
|
||||
StartGameDownloadPayload,
|
||||
} from "@types";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
/* Torrenting */
|
||||
startGameDownload: (
|
||||
repackId: number,
|
||||
objectID: string,
|
||||
title: string,
|
||||
shop: GameShop,
|
||||
downloadPath: string
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"startGameDownload",
|
||||
repackId,
|
||||
objectID,
|
||||
title,
|
||||
shop,
|
||||
downloadPath
|
||||
),
|
||||
startGameDownload: (payload: StartGameDownloadPayload) =>
|
||||
ipcRenderer.invoke("startGameDownload", payload),
|
||||
cancelGameDownload: (gameId: number) =>
|
||||
ipcRenderer.invoke("cancelGameDownload", gameId),
|
||||
pauseGameDownload: (gameId: number) =>
|
||||
ipcRenderer.invoke("pauseGameDownload", gameId),
|
||||
resumeGameDownload: (gameId: number) =>
|
||||
ipcRenderer.invoke("resumeGameDownload", gameId),
|
||||
onDownloadProgress: (cb: (value: TorrentProgress) => void) => {
|
||||
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
value: TorrentProgress
|
||||
value: DownloadProgress
|
||||
) => cb(value);
|
||||
ipcRenderer.on("on-download-progress", listener);
|
||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||
@ -61,6 +49,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
updateUserPreferences: (preferences: UserPreferences) =>
|
||||
ipcRenderer.invoke("updateUserPreferences", preferences),
|
||||
autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
|
||||
authenticateRealDebrid: (apiToken: string) =>
|
||||
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (
|
||||
@ -84,6 +74,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
||||
removeGameFromLibrary: (gameId: number) =>
|
||||
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
||||
removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId),
|
||||
deleteGameFolder: (gameId: number) =>
|
||||
ipcRenderer.invoke("deleteGameFolder", gameId),
|
||||
getGameByObjectID: (objectID: string) =>
|
||||
@ -115,10 +106,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
platform: process.platform,
|
||||
|
||||
/* Auto update */
|
||||
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => {
|
||||
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvent) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
value: AppUpdaterEvents
|
||||
value: AppUpdaterEvent
|
||||
) => cb(value);
|
||||
|
||||
ipcRenderer.on("autoUpdaterEvent", listener);
|
||||
|
@ -26,9 +26,9 @@ globalStyle("body", {
|
||||
overflow: "hidden",
|
||||
userSelect: "none",
|
||||
fontFamily: "'Fira Mono', monospace",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
fontSize: vars.size.body,
|
||||
background: vars.color.background,
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
margin: "0",
|
||||
});
|
||||
|
||||
@ -68,7 +68,7 @@ globalStyle(
|
||||
);
|
||||
|
||||
globalStyle("label", {
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
fontSize: vars.size.body,
|
||||
});
|
||||
|
||||
globalStyle("input[type=number]", {
|
||||
@ -79,6 +79,10 @@ globalStyle("img", {
|
||||
WebkitUserDrag: "none",
|
||||
} as Record<string, string>);
|
||||
|
||||
globalStyle("progress[value]", {
|
||||
WebkitAppearance: "none",
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { Sidebar, BottomPanel, Header } from "@renderer/components";
|
||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||
|
||||
import {
|
||||
useAppDispatch,
|
||||
@ -18,8 +18,8 @@ import {
|
||||
clearSearch,
|
||||
setUserPreferences,
|
||||
toggleDraggingDisabled,
|
||||
closeToast,
|
||||
} from "@renderer/features";
|
||||
import { GameStatusHelper } from "@shared";
|
||||
|
||||
document.body.classList.add(themeClass);
|
||||
|
||||
@ -42,6 +42,7 @@ export function App() {
|
||||
const draggingDisabled = useAppSelector(
|
||||
(state) => state.window.draggingDisabled
|
||||
);
|
||||
const toast = useAppSelector((state) => state.toast);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
|
||||
@ -54,7 +55,7 @@ export function App() {
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onDownloadProgress(
|
||||
(downloadProgress) => {
|
||||
if (GameStatusHelper.isReady(downloadProgress.game.status)) {
|
||||
if (downloadProgress.game.progress === 1) {
|
||||
clearDownload();
|
||||
updateLibrary();
|
||||
return;
|
||||
@ -109,6 +110,10 @@ export function App() {
|
||||
});
|
||||
}, [dispatch, draggingDisabled]);
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
dispatch(closeToast());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{window.electron.platform === "win32" && (
|
||||
@ -132,7 +137,15 @@ export function App() {
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<BottomPanel />
|
||||
|
||||
<Toast
|
||||
visible={toast.visible}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={handleToastClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4.262 4.262 0 0 0 .02.433c.031.3.037.59.316.92c.027.033.311.245.311.245c.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2c0 0 .284-.211.311-.243c.28-.33.285-.621.316-.92a4.261 4.261 0 0 0 .02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.708 1.708 0 0 1 .591.108a1.745 1.745 0 0 1 .49.299l-.452.546a1.247 1.247 0 0 0-.308-.195a.91.91 0 0 0-.363-.068a.658.658 0 0 0-.28.06a.703.703 0 0 0-.224.163a.783.783 0 0 0-.151.243a.799.799 0 0 0-.056.299v.008a.852.852 0 0 0 .056.31a.7.7 0 0 0 .157.245a.736.736 0 0 0 .238.16a.774.774 0 0 0 .303.058a.79.79 0 0 0 .445-.116v-.339h-.548v-.565H7.37v1.255a2.019 2.019 0 0 1-.524.307a1.789 1.789 0 0 1-.683.123a1.642 1.642 0 0 1-.602-.107a1.46 1.46 0 0 1-.478-.3a1.371 1.371 0 0 1-.318-.455a1.438 1.438 0 0 1-.115-.58v-.008a1.426 1.426 0 0 1 .113-.57a1.449 1.449 0 0 1 .312-.46a1.418 1.418 0 0 1 .474-.309a1.58 1.58 0 0 1 .598-.111a1.708 1.708 0 0 1 .045 0zm11.963.008a2.006 2.006 0 0 1 .612.094a1.61 1.61 0 0 1 .507.277l-.386.546a1.562 1.562 0 0 0-.39-.205a1.178 1.178 0 0 0-.388-.07a.347.347 0 0 0-.208.052a.154.154 0 0 0-.07.127v.008a.158.158 0 0 0 .022.084a.198.198 0 0 0 .076.066a.831.831 0 0 0 .147.06c.062.02.14.04.236.061a3.389 3.389 0 0 1 .43.122a1.292 1.292 0 0 1 .328.17a.678.678 0 0 1 .207.24a.739.739 0 0 1 .071.337v.008a.865.865 0 0 1-.081.382a.82.82 0 0 1-.229.285a1.032 1.032 0 0 1-.353.18a1.606 1.606 0 0 1-.46.061a2.16 2.16 0 0 1-.71-.116a1.718 1.718 0 0 1-.593-.346l.43-.514c.277.223.578.335.9.335a.457.457 0 0 0 .236-.05a.157.157 0 0 0 .082-.142v-.008a.15.15 0 0 0-.02-.077a.204.204 0 0 0-.073-.066a.753.753 0 0 0-.143-.062a2.45 2.45 0 0 0-.233-.062a5.036 5.036 0 0 1-.413-.113a1.26 1.26 0 0 1-.331-.16a.72.72 0 0 1-.222-.243a.73.73 0 0 1-.082-.36v-.008a.863.863 0 0 1 .074-.359a.794.794 0 0 1 .214-.283a1.007 1.007 0 0 1 .34-.185a1.423 1.423 0 0 1 .448-.066a2.006 2.006 0 0 1 .025 0m-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08l.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z"/></svg>
|
Before Width: | Height: | Size: 2.9 KiB |
@ -1 +0,0 @@
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M41.4193 7.30899C41.4193 7.30899 45.3046 5.79399 44.9808 9.47328C44.8729 10.9883 43.9016 16.2908 43.1461 22.0262L40.5559 39.0159C40.5559 39.0159 40.3401 41.5048 38.3974 41.9377C36.4547 42.3705 33.5408 40.4227 33.0011 39.9898C32.5694 39.6652 24.9068 34.7955 22.2086 32.4148C21.4531 31.7655 20.5897 30.4669 22.3165 28.9519L33.6487 18.1305C34.9438 16.8319 36.2389 13.8019 30.8426 17.4812L15.7331 27.7616C15.7331 27.7616 14.0063 28.8437 10.7686 27.8698L3.75342 25.7055C3.75342 25.7055 1.16321 24.0823 5.58815 22.459C16.3807 17.3729 29.6555 12.1786 41.4193 7.30899Z" fill="currentColor"></path> </g></svg>
|
Before Width: | Height: | Size: 838 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="16px" height="16px"><g fill="currentColor" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M5.91992,6l14.66211,21.375l-14.35156,16.625h3.17969l12.57617,-14.57812l10,14.57813h12.01367l-15.31836,-22.33008l13.51758,-15.66992h-3.16992l-11.75391,13.61719l-9.3418,-13.61719zM9.7168,8h7.16406l23.32227,34h-7.16406z"></path></g></g></svg>
|
Before Width: | Height: | Size: 702 B |
@ -43,5 +43,11 @@ export const backdrop = recipe({
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
},
|
||||
windows: {
|
||||
true: {
|
||||
// SPACING_UNIT * 3 + title bar spacing
|
||||
paddingTop: `${SPACING_UNIT * 3 + 35}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -7,6 +7,13 @@ export interface BackdropProps {
|
||||
|
||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||
return (
|
||||
<div className={styles.backdrop({ closing: isClosing })}>{children}</div>
|
||||
<div
|
||||
className={styles.backdrop({
|
||||
closing: isClosing,
|
||||
windows: window.electron.platform === "win32",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,19 +4,21 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
export const bottomPanel = style({
|
||||
width: "100%",
|
||||
borderTop: `solid 1px ${vars.color.border}`,
|
||||
backgroundColor: vars.color.background,
|
||||
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
transition: "all ease 0.2s",
|
||||
justifyContent: "space-between",
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const downloadsButton = style({
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
borderBottom: "1px solid transparent",
|
||||
":hover": {
|
||||
borderBottom: `1px solid ${vars.color.bodyText}`,
|
||||
borderBottom: `1px solid ${vars.color.body}`,
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
@ -1,23 +1,21 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
|
||||
import * as styles from "./bottom-panel.css";
|
||||
import { vars } from "../../theme.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { VERSION_CODENAME } from "@renderer/constants";
|
||||
import { GameStatus, GameStatusHelper } from "@shared";
|
||||
|
||||
export function BottomPanel() {
|
||||
const { t } = useTranslation("bottom_panel");
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { game, progress, downloadSpeed, eta } = useDownload();
|
||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
game && GameStatusHelper.isDownloading(game.status ?? null);
|
||||
const isGameDownloading = !!lastPacket?.game;
|
||||
|
||||
const [version, setVersion] = useState("");
|
||||
|
||||
@ -27,17 +25,18 @@ export function BottomPanel() {
|
||||
|
||||
const status = useMemo(() => {
|
||||
if (isGameDownloading) {
|
||||
if (game.status === GameStatus.DownloadingMetadata)
|
||||
return t("downloading_metadata", { title: game.title });
|
||||
if (lastPacket?.isDownloadingMetadata)
|
||||
return t("downloading_metadata", { title: lastPacket?.game.title });
|
||||
|
||||
if (game.status === GameStatus.CheckingFiles)
|
||||
return t("checking_files", {
|
||||
title: game.title,
|
||||
if (!eta) {
|
||||
return t("calculating_eta", {
|
||||
title: lastPacket?.game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
}
|
||||
|
||||
return t("downloading", {
|
||||
title: game?.title,
|
||||
title: lastPacket?.game.title,
|
||||
percentage: progress,
|
||||
eta,
|
||||
speed: downloadSpeed,
|
||||
@ -45,17 +44,18 @@ export function BottomPanel() {
|
||||
}
|
||||
|
||||
return t("no_downloads_in_progress");
|
||||
}, [t, isGameDownloading, game, progress, eta, downloadSpeed]);
|
||||
}, [
|
||||
t,
|
||||
isGameDownloading,
|
||||
lastPacket?.game,
|
||||
lastPacket?.isDownloadingMetadata,
|
||||
progress,
|
||||
eta,
|
||||
downloadSpeed,
|
||||
]);
|
||||
|
||||
return (
|
||||
<footer
|
||||
className={styles.bottomPanel}
|
||||
style={{
|
||||
background: isGameDownloading
|
||||
? `linear-gradient(90deg, ${vars.color.background} ${progress}, ${vars.color.darkBackground} ${progress})`
|
||||
: vars.color.darkBackground,
|
||||
}}
|
||||
>
|
||||
<footer className={styles.bottomPanel}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.downloadsButton}
|
||||
|
@ -18,7 +18,6 @@ const base = style({
|
||||
},
|
||||
":disabled": {
|
||||
opacity: vars.opacity.disabled,
|
||||
pointerEvents: "none",
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
});
|
||||
@ -30,6 +29,9 @@ export const button = styleVariants({
|
||||
":hover": {
|
||||
backgroundColor: "#DADBE1",
|
||||
},
|
||||
":disabled": {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
},
|
||||
],
|
||||
outline: [
|
||||
@ -41,6 +43,9 @@ export const button = styleVariants({
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
":disabled": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
],
|
||||
dark: [
|
||||
|
@ -109,6 +109,6 @@ export const shopIcon = style({
|
||||
});
|
||||
|
||||
export const noDownloadsLabel = style({
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
fontWeight: "bold",
|
||||
});
|
||||
|
@ -2,7 +2,6 @@ import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||
import type { CatalogueEntry } from "@types";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react";
|
||||
|
||||
import * as styles from "./game-card.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -16,7 +15,6 @@ export interface GameCardProps
|
||||
}
|
||||
|
||||
const shopIcon = {
|
||||
epic: <EpicGamesLogo className={styles.shopIcon} />,
|
||||
steam: <SteamLogo className={styles.shopIcon} />,
|
||||
};
|
||||
|
||||
|
@ -108,7 +108,7 @@ export const section = style({
|
||||
|
||||
export const backButton = recipe({
|
||||
base: {
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
WebkitAppRegion: "no-drag",
|
||||
position: "absolute",
|
||||
@ -156,10 +156,10 @@ export const newVersionButton = style({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
borderBottom: "1px solid transparent",
|
||||
":hover": {
|
||||
borderBottom: `1px solid ${vars.color.bodyText}`,
|
||||
borderBottom: `1px solid ${vars.color.body}`,
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
@ -80,7 +80,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const focusInput = () => {
|
||||
setIsFocused(true);
|
||||
|
@ -8,3 +8,4 @@ export * from "./sidebar/sidebar";
|
||||
export * from "./text-field/text-field";
|
||||
export * from "./checkbox-field/checkbox-field";
|
||||
export * from "./link/link";
|
||||
export * from "./toast/toast";
|
||||
|
@ -2,14 +2,14 @@ import { keyframes, style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const modalSlideIn = keyframes({
|
||||
export const fadeIn = keyframes({
|
||||
"0%": { opacity: 0 },
|
||||
"100%": {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export const modalSlideOut = keyframes({
|
||||
export const fadeOut = keyframes({
|
||||
"0%": { opacity: 1 },
|
||||
"100%": {
|
||||
opacity: 0,
|
||||
@ -18,12 +18,12 @@ export const modalSlideOut = keyframes({
|
||||
|
||||
export const modal = recipe({
|
||||
base: {
|
||||
animationName: modalSlideIn,
|
||||
animationName: fadeIn,
|
||||
animationDuration: "0.3s",
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "5px",
|
||||
maxWidth: "600px",
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
maxHeight: "100%",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
@ -33,7 +33,7 @@ export const modal = recipe({
|
||||
variants: {
|
||||
closing: {
|
||||
true: {
|
||||
animationName: modalSlideOut,
|
||||
animationName: fadeOut,
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
@ -65,5 +65,5 @@ export const closeModalButton = style({
|
||||
});
|
||||
|
||||
export const closeModalButtonIcon = style({
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
});
|
||||
|
@ -27,7 +27,7 @@ export const content = recipe({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
paddingBottom: "0",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
},
|
||||
@ -118,7 +118,6 @@ export const sectionTitle = style({
|
||||
});
|
||||
|
||||
export const section = style({
|
||||
padding: `${SPACING_UNIT * 2}px 0`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
@ -10,7 +10,6 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { routes } from "./routes";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { GameStatus, GameStatusHelper } from "@shared";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
@ -35,14 +34,14 @@ export function Sidebar() {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const { game: gameDownloading, progress } = useDownload();
|
||||
const { lastPacket, progress } = useDownload();
|
||||
|
||||
useEffect(() => {
|
||||
updateLibrary();
|
||||
}, [gameDownloading?.id, updateLibrary]);
|
||||
}, [lastPacket?.game.id, updateLibrary]);
|
||||
|
||||
const isDownloading = library.some((game) =>
|
||||
GameStatusHelper.isDownloading(game.status)
|
||||
const isDownloading = library.some(
|
||||
(game) => game.status === "active" && game.progress !== 1
|
||||
);
|
||||
|
||||
const sidebarRef = useRef<HTMLElement>(null);
|
||||
@ -101,18 +100,9 @@ export function Sidebar() {
|
||||
}, [isResizing]);
|
||||
|
||||
const getGameTitle = (game: Game) => {
|
||||
if (game.status === GameStatus.Paused)
|
||||
return t("paused", { title: game.title });
|
||||
|
||||
if (gameDownloading?.id === game.id) {
|
||||
const isVerifying = GameStatusHelper.isVerifying(gameDownloading.status);
|
||||
|
||||
if (isVerifying)
|
||||
return t(gameDownloading.status!, {
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
if (game.status === "paused") return t("paused", { title: game.title });
|
||||
|
||||
if (lastPacket?.game.id === game.id) {
|
||||
return t("downloading", {
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
@ -183,7 +173,7 @@ export function Sidebar() {
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === GameStatus.Cancelled,
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
|
83
src/renderer/src/components/toast/toast.css.ts
Normal file
83
src/renderer/src/components/toast/toast.css.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
const TOAST_HEIGHT = 80;
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
|
||||
"100%": { transform: "translateY(0)" },
|
||||
});
|
||||
|
||||
export const slideOut = keyframes({
|
||||
"0%": { transform: `translateY(0)` },
|
||||
"100%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
|
||||
});
|
||||
|
||||
export const toast = recipe({
|
||||
base: {
|
||||
animationDuration: "0.2s",
|
||||
animationTimingFunction: "ease-in-out",
|
||||
maxHeight: TOAST_HEIGHT,
|
||||
position: "fixed",
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
right: `${SPACING_UNIT * 2}px`,
|
||||
/* Bottom panel height + 16px */
|
||||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
zIndex: "0",
|
||||
maxWidth: "500px",
|
||||
},
|
||||
variants: {
|
||||
closing: {
|
||||
true: {
|
||||
animationName: slideOut,
|
||||
transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)`,
|
||||
},
|
||||
false: {
|
||||
animationName: slideIn,
|
||||
transform: `translateY(0)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const toastContent = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const progress = style({
|
||||
width: "100%",
|
||||
height: "5px",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
});
|
||||
|
||||
export const closeButton = style({
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
margin: "0",
|
||||
});
|
||||
|
||||
export const successIcon = style({
|
||||
color: vars.color.success,
|
||||
});
|
||||
|
||||
export const errorIcon = style({
|
||||
color: vars.color.danger,
|
||||
});
|
103
src/renderer/src/components/toast/toast.tsx
Normal file
103
src/renderer/src/components/toast/toast.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
CheckCircleFillIcon,
|
||||
XCircleFillIcon,
|
||||
XIcon,
|
||||
} from "@primer/octicons-react";
|
||||
|
||||
import * as styles from "./toast.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export interface ToastProps {
|
||||
visible: boolean;
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const INITIAL_PROGRESS = 100;
|
||||
|
||||
export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [progress, setProgress] = useState(INITIAL_PROGRESS);
|
||||
|
||||
const closingAnimation = useRef(-1);
|
||||
const progressAnimation = useRef(-1);
|
||||
|
||||
const startAnimateClosing = useCallback(() => {
|
||||
setIsClosing(true);
|
||||
const zero = performance.now();
|
||||
|
||||
closingAnimation.current = requestAnimationFrame(
|
||||
function animateClosing(time) {
|
||||
if (time - zero <= 200) {
|
||||
closingAnimation.current = requestAnimationFrame(animateClosing);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const zero = performance.now();
|
||||
|
||||
progressAnimation.current = requestAnimationFrame(
|
||||
function animateProgress(time) {
|
||||
const elapsed = time - zero;
|
||||
|
||||
const progress = Math.min(elapsed / 2500, 1);
|
||||
const currentValue =
|
||||
INITIAL_PROGRESS + (0 - INITIAL_PROGRESS) * progress;
|
||||
|
||||
setProgress(currentValue);
|
||||
|
||||
if (progress < 1) {
|
||||
progressAnimation.current = requestAnimationFrame(animateProgress);
|
||||
} else {
|
||||
cancelAnimationFrame(progressAnimation.current);
|
||||
startAnimateClosing();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
setProgress(INITIAL_PROGRESS);
|
||||
cancelAnimationFrame(closingAnimation.current);
|
||||
cancelAnimationFrame(progressAnimation.current);
|
||||
setIsClosing(false);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [startAnimateClosing, visible]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.toast({ closing: isClosing })}>
|
||||
<div className={styles.toastContent}>
|
||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{type === "success" && (
|
||||
<CheckCircleFillIcon className={styles.successIcon} />
|
||||
)}
|
||||
|
||||
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
|
||||
<span style={{ fontWeight: "bold" }}>{message}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.closeButton}
|
||||
onClick={startAnimateClosing}
|
||||
aria-label="Close toast"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<progress className={styles.progress} value={progress} max={100} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1 +1,8 @@
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export const VERSION_CODENAME = "Exodus";
|
||||
|
||||
export const DOWNLOADER_NAME = {
|
||||
[Downloader.RealDebrid]: "Real-Debrid",
|
||||
[Downloader.Torrent]: "Torrent",
|
||||
};
|
||||
|
16
src/renderer/src/declaration.d.ts
vendored
16
src/renderer/src/declaration.d.ts
vendored
@ -8,8 +8,10 @@ import type {
|
||||
HowLongToBeatCategory,
|
||||
ShopDetails,
|
||||
Steam250Game,
|
||||
TorrentProgress,
|
||||
DownloadProgress,
|
||||
UserPreferences,
|
||||
StartGameDownloadPayload,
|
||||
RealDebridUser,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
@ -21,18 +23,12 @@ declare global {
|
||||
|
||||
interface Electron {
|
||||
/* Torrenting */
|
||||
startGameDownload: (
|
||||
repackId: number,
|
||||
objectID: string,
|
||||
title: string,
|
||||
shop: GameShop,
|
||||
downloadPath: string
|
||||
) => Promise<Game>;
|
||||
startGameDownload: (payload: StartGameDownloadPayload) => Promise<void>;
|
||||
cancelGameDownload: (gameId: number) => Promise<void>;
|
||||
pauseGameDownload: (gameId: number) => Promise<void>;
|
||||
resumeGameDownload: (gameId: number) => Promise<void>;
|
||||
onDownloadProgress: (
|
||||
cb: (value: TorrentProgress) => void
|
||||
cb: (value: DownloadProgress) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Catalogue */
|
||||
@ -67,6 +63,7 @@ declare global {
|
||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||
closeGame: (gameId: number) => Promise<boolean>;
|
||||
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
||||
removeGame: (gameId: number) => Promise<void>;
|
||||
deleteGameFolder: (gameId: number) => Promise<unknown>;
|
||||
getGameByObjectID: (objectID: string) => Promise<Game | null>;
|
||||
onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer;
|
||||
@ -78,6 +75,7 @@ declare global {
|
||||
preferences: Partial<UserPreferences>
|
||||
) => Promise<void>;
|
||||
autoLaunch: (enabled: boolean) => Promise<void>;
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
import type { TorrentProgress } from "@types";
|
||||
import type { DownloadProgress } from "@types";
|
||||
|
||||
export interface DownloadState {
|
||||
lastPacket: TorrentProgress | null;
|
||||
lastPacket: DownloadProgress | null;
|
||||
gameId: number | null;
|
||||
gamesWithDeletionInProgress: number[];
|
||||
}
|
||||
@ -18,7 +18,7 @@ export const downloadSlice = createSlice({
|
||||
name: "download",
|
||||
initialState,
|
||||
reducers: {
|
||||
setLastPacket: (state, action: PayloadAction<TorrentProgress>) => {
|
||||
setLastPacket: (state, action: PayloadAction<DownloadProgress>) => {
|
||||
state.lastPacket = action.payload;
|
||||
if (!state.gameId) state.gameId = action.payload.game.id;
|
||||
},
|
||||
|
@ -3,3 +3,4 @@ export * from "./library-slice";
|
||||
export * from "./use-preferences-slice";
|
||||
export * from "./download-slice";
|
||||
export * from "./window-slice";
|
||||
export * from "./toast-slice";
|
||||
|
32
src/renderer/src/features/toast-slice.ts
Normal file
32
src/renderer/src/features/toast-slice.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ToastProps } from "@renderer/components/toast/toast";
|
||||
|
||||
export interface ToastState {
|
||||
message: string;
|
||||
type: ToastProps["type"];
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const initialState: ToastState = {
|
||||
message: "",
|
||||
type: "success",
|
||||
visible: false,
|
||||
};
|
||||
|
||||
export const toastSlice = createSlice({
|
||||
name: "toast",
|
||||
initialState,
|
||||
reducers: {
|
||||
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
|
||||
state.message = action.payload.message;
|
||||
state.type = action.payload.type;
|
||||
state.visible = true;
|
||||
},
|
||||
closeToast: (state) => {
|
||||
state.visible = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { showToast, closeToast } = toastSlice.actions;
|
@ -1,4 +1,5 @@
|
||||
export * from "./use-download";
|
||||
export * from "./use-library";
|
||||
export * from "./use-date";
|
||||
export * from "./use-toast";
|
||||
export * from "./redux";
|
||||
|
@ -9,9 +9,9 @@ import {
|
||||
setGameDeleting,
|
||||
removeGameFromDeleting,
|
||||
} from "@renderer/features";
|
||||
import type { GameShop, TorrentProgress } from "@types";
|
||||
import type { DownloadProgress, StartGameDownloadPayload } from "@types";
|
||||
import { useDate } from "./use-date";
|
||||
import { GameStatus, GameStatusHelper, formatBytes } from "@shared";
|
||||
import { formatBytes } from "@shared";
|
||||
|
||||
export function useDownload() {
|
||||
const { updateLibrary } = useLibrary();
|
||||
@ -22,57 +22,54 @@ export function useDownload() {
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const startDownload = (
|
||||
repackId: number,
|
||||
objectID: string,
|
||||
title: string,
|
||||
shop: GameShop,
|
||||
downloadPath: string
|
||||
) =>
|
||||
window.electron
|
||||
.startGameDownload(repackId, objectID, title, shop, downloadPath)
|
||||
.then((game) => {
|
||||
dispatch(clearDownload());
|
||||
updateLibrary();
|
||||
|
||||
return game;
|
||||
});
|
||||
|
||||
const pauseDownload = (gameId: number) =>
|
||||
window.electron.pauseGameDownload(gameId).then(() => {
|
||||
const startDownload = (payload: StartGameDownloadPayload) =>
|
||||
window.electron.startGameDownload(payload).then((game) => {
|
||||
dispatch(clearDownload());
|
||||
updateLibrary();
|
||||
|
||||
return game;
|
||||
});
|
||||
|
||||
const resumeDownload = (gameId: number) =>
|
||||
window.electron.resumeGameDownload(gameId).then(() => {
|
||||
updateLibrary();
|
||||
});
|
||||
const pauseDownload = async (gameId: number) => {
|
||||
await window.electron.pauseGameDownload(gameId);
|
||||
await updateLibrary();
|
||||
dispatch(clearDownload());
|
||||
};
|
||||
|
||||
const cancelDownload = (gameId: number) =>
|
||||
window.electron.cancelGameDownload(gameId).then(() => {
|
||||
dispatch(clearDownload());
|
||||
const resumeDownload = async (gameId: number) => {
|
||||
await window.electron.resumeGameDownload(gameId);
|
||||
return updateLibrary();
|
||||
};
|
||||
|
||||
const cancelDownload = async (gameId: number) => {
|
||||
await window.electron.cancelGameDownload(gameId);
|
||||
dispatch(clearDownload());
|
||||
updateLibrary();
|
||||
};
|
||||
|
||||
const removeGameInstaller = async (gameId: number) => {
|
||||
dispatch(setGameDeleting(gameId));
|
||||
|
||||
try {
|
||||
await window.electron.deleteGameFolder(gameId);
|
||||
await window.electron.removeGame(gameId);
|
||||
updateLibrary();
|
||||
deleteGame(gameId);
|
||||
});
|
||||
} finally {
|
||||
dispatch(removeGameFromDeleting(gameId));
|
||||
}
|
||||
};
|
||||
|
||||
const removeGameFromLibrary = (gameId: number) =>
|
||||
window.electron.removeGameFromLibrary(gameId).then(() => {
|
||||
updateLibrary();
|
||||
});
|
||||
|
||||
const isVerifying = GameStatusHelper.isVerifying(
|
||||
lastPacket?.game.status ?? null
|
||||
);
|
||||
|
||||
const getETA = () => {
|
||||
if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) {
|
||||
return "";
|
||||
}
|
||||
if (!lastPacket || lastPacket.timeRemaining < 0) return "";
|
||||
|
||||
try {
|
||||
return formatDistance(
|
||||
addMilliseconds(new Date(), lastPacket?.timeRemaining ?? 1),
|
||||
addMilliseconds(new Date(), lastPacket.timeRemaining),
|
||||
new Date(),
|
||||
{ addSuffix: true }
|
||||
);
|
||||
@ -81,50 +78,24 @@ export function useDownload() {
|
||||
}
|
||||
};
|
||||
|
||||
const getProgress = () => {
|
||||
if (lastPacket?.game.status === GameStatus.CheckingFiles) {
|
||||
return formatDownloadProgress(lastPacket?.game.fileVerificationProgress);
|
||||
}
|
||||
|
||||
return formatDownloadProgress(lastPacket?.game.progress);
|
||||
};
|
||||
|
||||
const deleteGame = (gameId: number) =>
|
||||
window.electron
|
||||
.cancelGameDownload(gameId)
|
||||
.then(() => {
|
||||
dispatch(setGameDeleting(gameId));
|
||||
return window.electron.deleteGameFolder(gameId);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
updateLibrary();
|
||||
dispatch(removeGameFromDeleting(gameId));
|
||||
});
|
||||
|
||||
const isGameDeleting = (gameId: number) => {
|
||||
return gamesWithDeletionInProgress.includes(gameId);
|
||||
};
|
||||
|
||||
return {
|
||||
game: lastPacket?.game,
|
||||
bytesDownloaded: lastPacket?.game.bytesDownloaded,
|
||||
fileSize: lastPacket?.game.fileSize,
|
||||
isVerifying,
|
||||
gameId: lastPacket?.game.id,
|
||||
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
|
||||
progress: getProgress(),
|
||||
numPeers: lastPacket?.numPeers,
|
||||
numSeeds: lastPacket?.numSeeds,
|
||||
progress: formatDownloadProgress(lastPacket?.game.progress),
|
||||
lastPacket,
|
||||
eta: getETA(),
|
||||
startDownload,
|
||||
pauseDownload,
|
||||
resumeDownload,
|
||||
cancelDownload,
|
||||
removeGameFromLibrary,
|
||||
deleteGame,
|
||||
removeGameInstaller,
|
||||
isGameDeleting,
|
||||
clearDownload: () => dispatch(clearDownload()),
|
||||
setLastPacket: (packet: TorrentProgress) => dispatch(setLastPacket(packet)),
|
||||
setLastPacket: (packet: DownloadProgress) =>
|
||||
dispatch(setLastPacket(packet)),
|
||||
};
|
||||
}
|
||||
|
33
src/renderer/src/hooks/use-toast.ts
Normal file
33
src/renderer/src/hooks/use-toast.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { useCallback } from "react";
|
||||
import { useAppDispatch } from "./redux";
|
||||
import { showToast } from "@renderer/features";
|
||||
|
||||
export function useToast() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const showSuccessToast = useCallback(
|
||||
(message: string) => {
|
||||
dispatch(
|
||||
showToast({
|
||||
message,
|
||||
type: "success",
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const showErrorToast = useCallback(
|
||||
(message: string) => {
|
||||
dispatch(
|
||||
showToast({
|
||||
message,
|
||||
type: "error",
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return { showSuccessToast, showErrorToast };
|
||||
}
|
@ -2,19 +2,19 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
|
||||
import * as styles from "./delete-modal.css";
|
||||
import * as styles from "./delete-game-modal.css";
|
||||
|
||||
interface DeleteModalProps {
|
||||
interface DeleteGameModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
deleteGame: () => void;
|
||||
}
|
||||
|
||||
export function DeleteModal({
|
||||
export function DeleteGameModal({
|
||||
onClose,
|
||||
visible,
|
||||
deleteGame,
|
||||
}: DeleteModalProps) {
|
||||
}: DeleteGameModalProps) {
|
||||
const { t } = useTranslation("downloads");
|
||||
|
||||
const handleDeleteGame = () => {
|
@ -12,7 +12,7 @@ export const downloadTitleWrapper = style({
|
||||
export const downloadTitle = style({
|
||||
fontWeight: "bold",
|
||||
cursor: "pointer",
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
textAlign: "left",
|
||||
fontSize: "16px",
|
||||
display: "block",
|
||||
@ -29,7 +29,6 @@ export const downloaderName = style({
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignSelf: "flex-start",
|
||||
});
|
||||
|
||||
export const downloads = style({
|
||||
@ -46,9 +45,34 @@ export const downloadCover = style({
|
||||
width: "280px",
|
||||
minWidth: "280px",
|
||||
height: "auto",
|
||||
objectFit: "cover",
|
||||
objectPosition: "center",
|
||||
borderRight: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const downloadCoverContent = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "flex-end",
|
||||
});
|
||||
|
||||
export const downloadCoverBackdrop = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const downloadCoverImage = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: "-1",
|
||||
});
|
||||
|
||||
export const download = recipe({
|
||||
|
@ -2,21 +2,30 @@ import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button, TextField } from "@renderer/components";
|
||||
import { formatDownloadProgress, steamUrlBuilder } from "@renderer/helpers";
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
formatDownloadProgress,
|
||||
steamUrlBuilder,
|
||||
} from "@renderer/helpers";
|
||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||
import type { Game } from "@types";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./downloads.css";
|
||||
import { DeleteModal } from "./delete-modal";
|
||||
import { Downloader, GameStatus, GameStatusHelper, formatBytes } from "@shared";
|
||||
import { DeleteGameModal } from "./delete-game-modal";
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
|
||||
export function Downloads() {
|
||||
const { library, updateLibrary } = useLibrary();
|
||||
|
||||
const { t } = useTranslation("downloads");
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const gameToBeDeleted = useRef<number | null>(null);
|
||||
@ -26,15 +35,13 @@ export function Downloads() {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
const {
|
||||
game: gameDownloading,
|
||||
lastPacket,
|
||||
progress,
|
||||
numPeers,
|
||||
numSeeds,
|
||||
pauseDownload,
|
||||
resumeDownload,
|
||||
removeGameFromLibrary,
|
||||
cancelDownload,
|
||||
deleteGame,
|
||||
removeGameInstaller,
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
|
||||
@ -53,27 +60,22 @@ export function Downloads() {
|
||||
});
|
||||
|
||||
const getFinalDownloadSize = (game: Game) => {
|
||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
if (!game) return "N/A";
|
||||
if (game.fileSize) return formatBytes(game.fileSize);
|
||||
|
||||
if (gameDownloading?.fileSize && isGameDownloading)
|
||||
return formatBytes(gameDownloading.fileSize);
|
||||
if (lastPacket?.game.fileSize && isGameDownloading)
|
||||
return formatBytes(lastPacket?.game.fileSize);
|
||||
|
||||
return game.repack?.fileSize ?? "N/A";
|
||||
};
|
||||
|
||||
const downloaderName = {
|
||||
[Downloader.RealDebrid]: t("real_debrid"),
|
||||
[Downloader.Torrent]: t("torrent"),
|
||||
};
|
||||
|
||||
const getGameInfo = (game: Game) => {
|
||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const finalDownloadSize = getFinalDownloadSize(game);
|
||||
|
||||
if (isGameDeleting(game?.id)) {
|
||||
if (isGameDeleting(game.id)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
}
|
||||
|
||||
@ -82,39 +84,30 @@ export function Downloads() {
|
||||
<>
|
||||
<p>{progress}</p>
|
||||
|
||||
{gameDownloading?.status &&
|
||||
gameDownloading?.status !== GameStatus.Downloading ? (
|
||||
<p>{t(gameDownloading?.status)}</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{formatBytes(gameDownloading?.bytesDownloaded)} /{" "}
|
||||
{finalDownloadSize}
|
||||
</p>
|
||||
{game.downloader === Downloader.Torrent && (
|
||||
<p>
|
||||
{numPeers} peers / {numSeeds} seeds
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
<p>
|
||||
{formatBytes(lastPacket?.game.bytesDownloaded)} /{" "}
|
||||
{finalDownloadSize}
|
||||
</p>
|
||||
|
||||
{game.downloader === Downloader.Torrent && (
|
||||
<small>
|
||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||
</small>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (GameStatusHelper.isReady(game?.status)) {
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
<>
|
||||
<p>{game?.repack?.title}</p>
|
||||
<p>{game.repack?.title}</p>
|
||||
<p>{t("completed")}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (game?.status === GameStatus.Cancelled) return <p>{t("cancelled")}</p>;
|
||||
if (game?.status === GameStatus.DownloadingMetadata)
|
||||
return <p>{t("starting_download")}</p>;
|
||||
|
||||
if (game?.status === GameStatus.Paused) {
|
||||
if (game.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<p>{formatDownloadProgress(game.progress)}</p>
|
||||
@ -123,7 +116,19 @@ export function Downloads() {
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
if (game.status === "active") {
|
||||
return (
|
||||
<>
|
||||
<p>{formatDownloadProgress(game.progress)}</p>
|
||||
|
||||
<p>
|
||||
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>{t(game.status)}</p>;
|
||||
};
|
||||
|
||||
const openDeleteModal = (gameId: number) => {
|
||||
@ -132,37 +137,11 @@ export function Downloads() {
|
||||
};
|
||||
|
||||
const getGameActions = (game: Game) => {
|
||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
const deleting = isGameDeleting(game.id);
|
||||
|
||||
if (isGameDownloading) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.Paused) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => resumeDownload(game.id)} theme="outline">
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (GameStatusHelper.isReady(game?.status)) {
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@ -180,18 +159,43 @@ export function Downloads() {
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.DownloadingMetadata) {
|
||||
if (isGameDownloading || game.status === "active") {
|
||||
return (
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<>
|
||||
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => resumeDownload(game.id)}
|
||||
theme="outline"
|
||||
disabled={
|
||||
game.downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken
|
||||
}
|
||||
>
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => navigate(`/game/${game.shop}/${game.objectID}`)}
|
||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
>
|
||||
@ -219,10 +223,9 @@ export function Downloads() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteGame = () => {
|
||||
if (gameToBeDeleted.current) {
|
||||
deleteGame(gameToBeDeleted.current).then(updateLibrary);
|
||||
}
|
||||
const handleDeleteGame = async () => {
|
||||
if (gameToBeDeleted.current)
|
||||
await removeGameInstaller(gameToBeDeleted.current);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -231,7 +234,8 @@ export function Downloads() {
|
||||
visible={showBinaryNotFoundModal}
|
||||
onClose={() => setShowBinaryNotFoundModal(false)}
|
||||
/>
|
||||
<DeleteModal
|
||||
|
||||
<DeleteGameModal
|
||||
visible={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
deleteGame={handleDeleteGame}
|
||||
@ -245,31 +249,36 @@ export function Downloads() {
|
||||
<li
|
||||
key={game.id}
|
||||
className={styles.download({
|
||||
cancelled: game.status === GameStatus.Cancelled,
|
||||
cancelled: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={steamUrlBuilder.library(game.objectID)}
|
||||
className={styles.downloadCover}
|
||||
alt={game.title}
|
||||
/>
|
||||
<div className={styles.downloadCover}>
|
||||
<div className={styles.downloadCoverBackdrop}>
|
||||
<img
|
||||
src={steamUrlBuilder.library(game.objectID)}
|
||||
className={styles.downloadCoverImage}
|
||||
alt={game.title}
|
||||
/>
|
||||
|
||||
<div className={styles.downloadCoverContent}>
|
||||
<small className={styles.downloaderName}>
|
||||
{DOWNLOADER_NAME[game.downloader]}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.downloadRightContent}>
|
||||
<div className={styles.downloadDetails}>
|
||||
<div className={styles.downloadTitleWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.downloadTitle}
|
||||
onClick={() =>
|
||||
navigate(`/game/${game.shop}/${game.objectID}`)
|
||||
}
|
||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||
>
|
||||
{game.title}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<small className={styles.downloaderName}>
|
||||
{downloaderName[game?.downloader]}
|
||||
</small>
|
||||
{getGameInfo(game)}
|
||||
</div>
|
||||
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { ShopDetails } from "@types";
|
||||
|
||||
import * as styles from "./game-details.css";
|
||||
import { useContext } from "react";
|
||||
import { gameDetailsContext } from "./game-details.context";
|
||||
|
||||
export interface DescriptionHeaderProps {
|
||||
gameDetails: ShopDetails;
|
||||
}
|
||||
export function DescriptionHeader() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
||||
|
||||
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
if (!shopDetails) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.descriptionHeader}>
|
||||
<section className={styles.descriptionHeaderInfo}>
|
||||
<p>
|
||||
{t("release_date", {
|
||||
date: gameDetails?.release_date.date,
|
||||
date: shopDetails?.release_date.date,
|
||||
})}
|
||||
</p>
|
||||
<p>{t("publisher", { publisher: gameDetails.publishers[0] })}</p>
|
||||
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
@ -67,6 +67,7 @@ export const mediaPreviewButton = recipe({
|
||||
transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
":hover": {
|
||||
opacity: "0.8",
|
||||
},
|
||||
@ -84,7 +85,6 @@ export const mediaPreview = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flex: "1",
|
||||
});
|
||||
|
||||
export const gallerySliderButton = recipe({
|
||||
|
@ -1,37 +1,36 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||
|
||||
import type { ShopDetails } from "@types";
|
||||
|
||||
import * as styles from "./gallery-slider.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "./game-details.context";
|
||||
|
||||
export interface GallerySliderProps {
|
||||
gameDetails: ShopDetails;
|
||||
}
|
||||
export function GallerySlider() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
||||
|
||||
export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const mediaContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const hasScreenshots = gameDetails && gameDetails.screenshots.length;
|
||||
const hasMovies = gameDetails && gameDetails.movies?.length;
|
||||
const hasScreenshots = shopDetails && shopDetails.screenshots.length;
|
||||
const hasMovies = shopDetails && shopDetails.movies?.length;
|
||||
|
||||
const [mediaCount] = useState<number>(() => {
|
||||
if (gameDetails.screenshots && gameDetails.movies) {
|
||||
return gameDetails.screenshots.length + gameDetails.movies.length;
|
||||
} else if (gameDetails.movies) {
|
||||
return gameDetails.movies.length;
|
||||
} else if (gameDetails.screenshots) {
|
||||
return gameDetails.screenshots.length;
|
||||
const mediaCount = useMemo(() => {
|
||||
if (!shopDetails) return 0;
|
||||
|
||||
if (shopDetails.screenshots && shopDetails.movies) {
|
||||
return shopDetails.screenshots.length + shopDetails.movies.length;
|
||||
} else if (shopDetails.movies) {
|
||||
return shopDetails.movies.length;
|
||||
} else if (shopDetails.screenshots) {
|
||||
return shopDetails.screenshots.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}, [shopDetails]);
|
||||
|
||||
const [mediaIndex, setMediaIndex] = useState<number>(0);
|
||||
const [mediaIndex, setMediaIndex] = useState(0);
|
||||
const [showArrows, setShowArrows] = useState(false);
|
||||
|
||||
const showNextImage = () => {
|
||||
@ -52,7 +51,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||
|
||||
useEffect(() => {
|
||||
setMediaIndex(0);
|
||||
}, [gameDetails]);
|
||||
}, [shopDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasMovies && mediaContainerRef.current) {
|
||||
@ -74,17 +73,17 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||
const scrollLeft = mediaIndex * itemWidth;
|
||||
container.scrollLeft = scrollLeft;
|
||||
}
|
||||
}, [gameDetails, mediaIndex, mediaCount]);
|
||||
}, [shopDetails, mediaIndex, mediaCount]);
|
||||
|
||||
const previews = useMemo(() => {
|
||||
const screenshotPreviews =
|
||||
gameDetails?.screenshots.map(({ id, path_thumbnail }) => ({
|
||||
shopDetails?.screenshots.map(({ id, path_thumbnail }) => ({
|
||||
id,
|
||||
thumbnail: path_thumbnail,
|
||||
})) ?? [];
|
||||
|
||||
if (gameDetails?.movies) {
|
||||
const moviePreviews = gameDetails.movies.map(({ id, thumbnail }) => ({
|
||||
if (shopDetails?.movies) {
|
||||
const moviePreviews = shopDetails.movies.map(({ id, thumbnail }) => ({
|
||||
id,
|
||||
thumbnail,
|
||||
}));
|
||||
@ -93,7 +92,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||
}
|
||||
|
||||
return screenshotPreviews;
|
||||
}, [gameDetails]);
|
||||
}, [shopDetails]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -105,8 +104,8 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||
className={styles.gallerySliderAnimationContainer}
|
||||
ref={mediaContainerRef}
|
||||
>
|
||||
{gameDetails.movies &&
|
||||
gameDetails.movies.map((video) => (
|
||||
{shopDetails.movies &&
|
||||
shopDetails.movies.map((video) => (
|
||||
<video
|
||||
key={video.id}
|
||||
controls
|
||||
@ -122,7 +121,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||
))}
|
||||
|
||||
{hasScreenshots &&
|
||||
gameDetails.screenshots.map((image, i) => (
|
||||
shopDetails.screenshots.map((image, i) => (
|
||||
<img
|
||||
key={image.id}
|
||||
className={styles.gallerySliderMedia}
|
||||
|
209
src/renderer/src/pages/game-details/game-details.context.tsx
Normal file
209
src/renderer/src/pages/game-details/game-details.context.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { getSteamLanguage } from "@renderer/helpers";
|
||||
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
||||
|
||||
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
DODIInstallationGuide,
|
||||
DONT_SHOW_DODI_INSTRUCTIONS_KEY,
|
||||
DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY,
|
||||
OnlineFixInstallationGuide,
|
||||
RepacksModal,
|
||||
} from "./modals";
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export interface GameDetailsContext {
|
||||
game: Game | null;
|
||||
shopDetails: ShopDetails | null;
|
||||
repacks: GameRepack[];
|
||||
gameTitle: string;
|
||||
isGameRunning: boolean;
|
||||
isLoading: boolean;
|
||||
objectID: string | undefined;
|
||||
gameColor: string;
|
||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
openRepacksModal: () => void;
|
||||
updateGame: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
game: null,
|
||||
shopDetails: null,
|
||||
repacks: [],
|
||||
gameTitle: "",
|
||||
isGameRunning: false,
|
||||
isLoading: false,
|
||||
objectID: undefined,
|
||||
gameColor: "",
|
||||
setGameColor: () => {},
|
||||
openRepacksModal: () => {},
|
||||
updateGame: async () => {},
|
||||
});
|
||||
|
||||
const { Provider } = gameDetailsContext;
|
||||
export const { Consumer: GameDetailsContextConsumer } = gameDetailsContext;
|
||||
|
||||
export interface GameDetailsContextProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function GameDetailsContextProvider({
|
||||
children,
|
||||
}: GameDetailsContextProps) {
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
const [shopDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [gameColor, setGameColor] = useState("");
|
||||
const [showInstructionsModal, setShowInstructionsModal] = useState<
|
||||
null | "onlinefix" | "DODI"
|
||||
>(null);
|
||||
const [isGameRunning, setisGameRunning] = useState(false);
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const gameTitle = searchParams.get("title")!;
|
||||
|
||||
const { i18n } = useTranslation("game_details");
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { startDownload, lastPacket } = useDownload();
|
||||
|
||||
const updateGame = useCallback(async () => {
|
||||
return window.electron
|
||||
.getGameByObjectID(objectID!)
|
||||
.then((result) => setGame(result));
|
||||
}, [setGame, objectID]);
|
||||
|
||||
const isGameDownloading = lastPacket?.game.id === game?.id;
|
||||
|
||||
useEffect(() => {
|
||||
updateGame();
|
||||
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
window.electron.getGameShopDetails(
|
||||
objectID!,
|
||||
shop as GameShop,
|
||||
getSteamLanguage(i18n.language)
|
||||
),
|
||||
window.electron.searchGameRepacks(gameTitle),
|
||||
])
|
||||
.then(([appDetails, repacks]) => {
|
||||
if (appDetails) setGameDetails(appDetails);
|
||||
setRepacks(repacks);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
updateGame();
|
||||
}, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]);
|
||||
|
||||
useEffect(() => {
|
||||
setGameDetails(null);
|
||||
setGame(null);
|
||||
setIsLoading(true);
|
||||
setisGameRunning(false);
|
||||
dispatch(setHeaderTitle(gameTitle));
|
||||
}, [objectID, gameTitle, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
window.electron.onGameClose(() => {
|
||||
if (isGameRunning) setisGameRunning(false);
|
||||
}),
|
||||
window.electron.onPlaytime((gameId) => {
|
||||
if (gameId === game?.id) {
|
||||
if (!isGameRunning) setisGameRunning(true);
|
||||
updateGame();
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
listeners.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [game?.id, isGameRunning, updateGame]);
|
||||
|
||||
const handleStartDownload = async (
|
||||
repack: GameRepack,
|
||||
downloader: Downloader,
|
||||
downloadPath: string
|
||||
) => {
|
||||
await startDownload({
|
||||
repackId: repack.id,
|
||||
objectID: objectID!,
|
||||
title: gameTitle,
|
||||
downloader,
|
||||
shop: shop as GameShop,
|
||||
downloadPath,
|
||||
});
|
||||
|
||||
await updateGame();
|
||||
setShowRepacksModal(false);
|
||||
|
||||
if (
|
||||
repack.repacker === "onlinefix" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("onlinefix");
|
||||
} else if (
|
||||
repack.repacker === "DODI" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("DODI");
|
||||
}
|
||||
};
|
||||
|
||||
const openRepacksModal = () => setShowRepacksModal(true);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
game,
|
||||
shopDetails,
|
||||
repacks,
|
||||
gameTitle,
|
||||
isGameRunning,
|
||||
isLoading,
|
||||
objectID,
|
||||
gameColor,
|
||||
setGameColor,
|
||||
openRepacksModal,
|
||||
updateGame,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
|
||||
<OnlineFixInstallationGuide
|
||||
visible={showInstructionsModal === "onlinefix"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
<DODIInstallationGuide
|
||||
visible={showInstructionsModal === "DODI"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</>
|
||||
</Provider>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@ import { globalStyle, keyframes, style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: `translateY(${40 + 16}px)` },
|
||||
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` },
|
||||
"100%": { transform: "translateY(0)" },
|
||||
});
|
||||
|
||||
@ -174,5 +174,5 @@ globalStyle(`${description} img`, {
|
||||
});
|
||||
|
||||
globalStyle(`${description} a`, {
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
});
|
||||
|
@ -1,24 +1,11 @@
|
||||
import Color from "color";
|
||||
import { average } from "color.js";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { average } from "color.js";
|
||||
|
||||
import {
|
||||
Steam250Game,
|
||||
type Game,
|
||||
type GameRepack,
|
||||
type GameShop,
|
||||
type ShopDetails,
|
||||
} from "@types";
|
||||
import { Steam250Game } from "@types";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
getSteamLanguage,
|
||||
steamUrlBuilder,
|
||||
} from "@renderer/helpers";
|
||||
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
||||
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
||||
|
||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||
|
||||
@ -29,153 +16,34 @@ import { DescriptionHeader } from "./description-header";
|
||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||
import * as styles from "./game-details.css";
|
||||
import { HeroPanel } from "./hero";
|
||||
import { RepacksModal } from "./repacks-modal";
|
||||
|
||||
import { vars } from "../../theme.css";
|
||||
import {
|
||||
DODIInstallationGuide,
|
||||
DONT_SHOW_DODI_INSTRUCTIONS_KEY,
|
||||
DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY,
|
||||
OnlineFixInstallationGuide,
|
||||
} from "./installation-guides";
|
||||
|
||||
import { GallerySlider } from "./gallery-slider";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
import {
|
||||
GameDetailsContextConsumer,
|
||||
GameDetailsContextProvider,
|
||||
} from "./game-details.context";
|
||||
|
||||
export function GameDetails() {
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
const [color, setColor] = useState({ dark: "", light: "" });
|
||||
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
const [isGamePlaying, setIsGamePlaying] = useState(false);
|
||||
const [showInstructionsModal, setShowInstructionsModal] = useState<
|
||||
null | "onlinefix" | "DODI"
|
||||
>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { objectID } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const fromRandomizer = searchParams.get("fromRandomizer");
|
||||
const title = searchParams.get("title")!;
|
||||
|
||||
const { t, i18n } = useTranslation("game_details");
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { game: gameDownloading, startDownload } = useDownload();
|
||||
|
||||
const heroImage = steamUrlBuilder.libraryHero(objectID!);
|
||||
|
||||
const handleHeroLoad = () => {
|
||||
average(heroImage, { amount: 1, format: "hex" })
|
||||
.then((color) => {
|
||||
const darkColor = new Color(color).darken(0.6).toString() as string;
|
||||
setColor({ light: color as string, dark: darkColor });
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const getGame = useCallback(() => {
|
||||
window.electron
|
||||
.getGameByObjectID(objectID!)
|
||||
.then((result) => setGame(result));
|
||||
}, [setGame, objectID]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
getGame();
|
||||
}, [getGame, gameDownloading?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setGameDetails(null);
|
||||
setGame(null);
|
||||
setIsLoading(true);
|
||||
setIsGamePlaying(false);
|
||||
dispatch(setHeaderTitle(title));
|
||||
|
||||
setRandomGame(null);
|
||||
window.electron.getRandomGame().then((randomGame) => {
|
||||
setRandomGame(randomGame);
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
window.electron.getGameShopDetails(
|
||||
objectID!,
|
||||
"steam",
|
||||
getSteamLanguage(i18n.language)
|
||||
),
|
||||
window.electron.searchGameRepacks(title),
|
||||
])
|
||||
.then(([appDetails, repacks]) => {
|
||||
if (appDetails) setGameDetails(appDetails);
|
||||
setRepacks(repacks);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
getGame();
|
||||
}, [getGame, dispatch, navigate, title, objectID, i18n.language]);
|
||||
|
||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (isGameDownloading)
|
||||
setGame((prev) => {
|
||||
if (prev === null || !gameDownloading?.status) return prev;
|
||||
return { ...prev, status: gameDownloading?.status };
|
||||
});
|
||||
}, [isGameDownloading, gameDownloading?.status]);
|
||||
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
window.electron.onGameClose(() => {
|
||||
if (isGamePlaying) setIsGamePlaying(false);
|
||||
}),
|
||||
window.electron.onPlaytime((gameId) => {
|
||||
if (gameId === game?.id) {
|
||||
if (!isGamePlaying) setIsGamePlaying(true);
|
||||
getGame();
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
listeners.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [game?.id, isGamePlaying, getGame]);
|
||||
|
||||
const handleStartDownload = async (
|
||||
repack: GameRepack,
|
||||
downloadPath: string
|
||||
) => {
|
||||
return startDownload(
|
||||
repack.id,
|
||||
objectID!,
|
||||
title,
|
||||
shop as GameShop,
|
||||
downloadPath
|
||||
).then(() => {
|
||||
getGame();
|
||||
setShowRepacksModal(false);
|
||||
|
||||
if (
|
||||
repack.repacker === "onlinefix" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("onlinefix");
|
||||
} else if (
|
||||
repack.repacker === "DODI" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("DODI");
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [objectID]);
|
||||
|
||||
const handleRandomizerClick = () => {
|
||||
if (randomGame) {
|
||||
@ -189,97 +57,95 @@ export function GameDetails() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
repacks={repacks}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
<GameDetailsContextProvider>
|
||||
<GameDetailsContextConsumer>
|
||||
{({ game, shopDetails, isLoading, setGameColor }) => {
|
||||
const handleHeroLoad = async () => {
|
||||
const output = await average(
|
||||
steamUrlBuilder.libraryHero(objectID!),
|
||||
{
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
}
|
||||
);
|
||||
|
||||
<OnlineFixInstallationGuide
|
||||
visible={showInstructionsModal === "onlinefix"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
setGameColor(output as string);
|
||||
};
|
||||
|
||||
<DODIInstallationGuide
|
||||
windowColor={color.light}
|
||||
visible={showInstructionsModal === "DODI"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
>
|
||||
{isLoading ? (
|
||||
<GameDetailsSkeleton />
|
||||
) : (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||
className={styles.heroImage}
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
<div className={styles.heroBackdrop}>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectID!)}
|
||||
style={{ width: 300, alignSelf: "flex-end" }}
|
||||
alt={game?.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<GameDetailsSkeleton />
|
||||
) : (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<img
|
||||
src={heroImage}
|
||||
className={styles.heroImage}
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
<div className={styles.heroBackdrop}>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectID!)}
|
||||
style={{ width: 300, alignSelf: "flex-end" }}
|
||||
alt={game?.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HeroPanel />
|
||||
|
||||
<HeroPanel
|
||||
game={game}
|
||||
color={color.dark}
|
||||
objectID={objectID!}
|
||||
title={title}
|
||||
repacks={repacks}
|
||||
openRepacksModal={() => setShowRepacksModal(true)}
|
||||
getGame={getGame}
|
||||
isGamePlaying={isGamePlaying}
|
||||
/>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
<DescriptionHeader />
|
||||
<GallerySlider />
|
||||
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
{gameDetails && <DescriptionHeader gameDetails={gameDetails} />}
|
||||
{gameDetails && <GallerySlider gameDetails={gameDetails} />}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.about_the_game ?? t("no_shop_details"),
|
||||
}}
|
||||
className={styles.description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: gameDetails?.about_the_game ?? t("no_shop_details"),
|
||||
}}
|
||||
className={styles.description}
|
||||
/>
|
||||
</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Sidebar
|
||||
objectID={objectID!}
|
||||
title={title}
|
||||
gameDetails={gameDetails}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{fromRandomizer && (
|
||||
<Button
|
||||
className={styles.randomizerButton}
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame}
|
||||
>
|
||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||
<Lottie
|
||||
animationData={starsAnimation}
|
||||
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
{t("next_suggestion")}
|
||||
</Button>
|
||||
)}
|
||||
</SkeletonTheme>
|
||||
{fromRandomizer && (
|
||||
<Button
|
||||
className={styles.randomizerButton}
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame}
|
||||
>
|
||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||
<Lottie
|
||||
animationData={starsAnimation}
|
||||
style={{
|
||||
width: 70,
|
||||
position: "absolute",
|
||||
top: -28,
|
||||
left: -27,
|
||||
}}
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
{t("next_suggestion")}
|
||||
</Button>
|
||||
)}
|
||||
</SkeletonTheme>
|
||||
);
|
||||
}}
|
||||
</GameDetailsContextConsumer>
|
||||
</GameDetailsContextProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,39 +1,20 @@
|
||||
import { GameStatus, GameStatusHelper } from "@shared";
|
||||
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||
|
||||
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import type { Game, GameRepack } from "@types";
|
||||
import { useState } from "react";
|
||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./hero-panel-actions.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export interface HeroPanelActionsProps {
|
||||
game: Game | null;
|
||||
repacks: GameRepack[];
|
||||
isGamePlaying: boolean;
|
||||
isGameDownloading: boolean;
|
||||
objectID: string;
|
||||
title: string;
|
||||
openRepacksModal: () => void;
|
||||
openBinaryNotFoundModal: () => void;
|
||||
getGame: () => void;
|
||||
}
|
||||
|
||||
export function HeroPanelActions({
|
||||
game,
|
||||
isGamePlaying,
|
||||
isGameDownloading,
|
||||
repacks,
|
||||
objectID,
|
||||
title,
|
||||
openRepacksModal,
|
||||
openBinaryNotFoundModal,
|
||||
getGame,
|
||||
}: HeroPanelActionsProps) {
|
||||
export function HeroPanelActions() {
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
useState(false);
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
|
||||
const {
|
||||
resumeDownload,
|
||||
@ -43,12 +24,25 @@ export function HeroPanelActions({
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
|
||||
const {
|
||||
game,
|
||||
repacks,
|
||||
isGameRunning,
|
||||
objectID,
|
||||
gameTitle,
|
||||
openRepacksModal,
|
||||
updateGame,
|
||||
} = useContext(gameDetailsContext);
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const getDownloadsPath = async () => {
|
||||
const userPreferences = await window.electron.getUserPreferences();
|
||||
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||
return window.electron.getDefaultDownloadsPath();
|
||||
};
|
||||
@ -86,15 +80,15 @@ export function HeroPanelActions({
|
||||
const gameExecutablePath = await selectGameExecutable();
|
||||
|
||||
await window.electron.addGameToLibrary(
|
||||
objectID,
|
||||
title,
|
||||
objectID!,
|
||||
gameTitle,
|
||||
"steam",
|
||||
gameExecutablePath
|
||||
);
|
||||
}
|
||||
|
||||
updateLibrary();
|
||||
getGame();
|
||||
updateGame();
|
||||
} finally {
|
||||
setToggleLibraryGameDisabled(false);
|
||||
}
|
||||
@ -103,7 +97,7 @@ export function HeroPanelActions({
|
||||
const openGameInstaller = () => {
|
||||
if (game) {
|
||||
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
|
||||
if (!isBinaryInPath) openBinaryNotFoundModal();
|
||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||
updateLibrary();
|
||||
});
|
||||
}
|
||||
@ -145,18 +139,18 @@ export function HeroPanelActions({
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (game && isGameDownloading) {
|
||||
if (game?.status === "active" && game?.progress !== 1) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => pauseDownload(game.id)}
|
||||
onClick={() => pauseDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id)}
|
||||
onClick={() => cancelDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
@ -166,18 +160,22 @@ export function HeroPanelActions({
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.Paused) {
|
||||
if (game?.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => resumeDownload(game.id)}
|
||||
onClick={() => resumeDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
disabled={
|
||||
game.downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken
|
||||
}
|
||||
>
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id).then(getGame)}
|
||||
onClick={() => cancelDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
@ -187,49 +185,7 @@ export function HeroPanelActions({
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
GameStatusHelper.isReady(game?.status ?? null) ||
|
||||
(game && !game.status)
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{GameStatusHelper.isReady(game?.status ?? null) ? (
|
||||
<Button
|
||||
onClick={openGameInstaller}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("install")}
|
||||
</Button>
|
||||
) : (
|
||||
toggleGameOnLibraryButton
|
||||
)}
|
||||
|
||||
{isGamePlaying ? (
|
||||
<Button
|
||||
onClick={closeGame}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("close")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={openGame}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("play")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.Cancelled) {
|
||||
if (game?.status === "removed") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@ -240,8 +196,9 @@ export function HeroPanelActions({
|
||||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => removeGameFromLibrary(game.id).then(getGame)}
|
||||
onClick={() => removeGameFromLibrary(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
@ -252,7 +209,7 @@ export function HeroPanelActions({
|
||||
);
|
||||
}
|
||||
|
||||
if (repacks.length) {
|
||||
if (repacks.length && !game) {
|
||||
return (
|
||||
<>
|
||||
{toggleGameOnLibraryButton}
|
||||
@ -267,5 +224,47 @@ export function HeroPanelActions({
|
||||
);
|
||||
}
|
||||
|
||||
return toggleGameOnLibraryButton;
|
||||
return (
|
||||
<>
|
||||
{game?.progress === 1 ? (
|
||||
<>
|
||||
<BinaryNotFoundModal
|
||||
visible={showBinaryNotFoundModal}
|
||||
onClose={() => setShowBinaryNotFoundModal(false)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={openGameInstaller}
|
||||
theme="outline"
|
||||
disabled={deleting || isGameRunning}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("install")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
toggleGameOnLibraryButton
|
||||
)}
|
||||
|
||||
{isGameRunning ? (
|
||||
<Button
|
||||
onClick={closeGame}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("close")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={openGame}
|
||||
theme="outline"
|
||||
disabled={deleting || isGameRunning}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("play")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,22 +1,16 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { Game } from "@types";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
export interface HeroPanelPlaytimeProps {
|
||||
game: Game;
|
||||
isGamePlaying: boolean;
|
||||
}
|
||||
|
||||
export function HeroPanelPlaytime({
|
||||
game,
|
||||
isGamePlaying,
|
||||
}: HeroPanelPlaytimeProps) {
|
||||
export function HeroPanelPlaytime() {
|
||||
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
||||
|
||||
const { game, isGameRunning } = useContext(gameDetailsContext);
|
||||
|
||||
const { i18n, t } = useTranslation("game_details");
|
||||
|
||||
const { formatDistance } = useDate();
|
||||
@ -52,8 +46,8 @@ export function HeroPanelPlaytime({
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
};
|
||||
|
||||
if (!game.lastTimePlayed) {
|
||||
return <p>{t("not_played_yet", { title: game.title })}</p>;
|
||||
if (!game?.lastTimePlayed) {
|
||||
return <p>{t("not_played_yet", { title: game?.title })}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -64,7 +58,7 @@ export function HeroPanelPlaytime({
|
||||
})}
|
||||
</p>
|
||||
|
||||
{isGamePlaying ? (
|
||||
{isGameRunning ? (
|
||||
<p>{t("playing_now")}</p>
|
||||
) : (
|
||||
<p>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const panel = style({
|
||||
width: "100%",
|
||||
@ -11,6 +12,8 @@ export const panel = style({
|
||||
justifyContent: "space-between",
|
||||
transition: "all ease 0.2s",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
@ -29,3 +32,27 @@ export const downloadDetailsRow = style({
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
});
|
||||
|
||||
export const progressBar = recipe({
|
||||
base: {
|
||||
position: "absolute",
|
||||
bottom: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "3px",
|
||||
transition: "all ease 0.2s",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
disabled: {
|
||||
true: {
|
||||
opacity: vars.opacity.disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,105 +1,92 @@
|
||||
import { format } from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Color from "color";
|
||||
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import type { Game, GameRepack } from "@types";
|
||||
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { HeroPanelActions } from "./hero-panel-actions";
|
||||
import { Downloader, GameStatus, GameStatusHelper, formatBytes } from "@shared";
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
|
||||
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
export interface HeroPanelProps {
|
||||
game: Game | null;
|
||||
color: string;
|
||||
isGamePlaying: boolean;
|
||||
objectID: string;
|
||||
title: string;
|
||||
repacks: GameRepack[];
|
||||
openRepacksModal: () => void;
|
||||
getGame: () => void;
|
||||
}
|
||||
|
||||
export function HeroPanel({
|
||||
game,
|
||||
color,
|
||||
repacks,
|
||||
objectID,
|
||||
title,
|
||||
isGamePlaying,
|
||||
openRepacksModal,
|
||||
getGame,
|
||||
}: HeroPanelProps) {
|
||||
export function HeroPanel() {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
||||
|
||||
const {
|
||||
game: gameDownloading,
|
||||
progress,
|
||||
eta,
|
||||
numPeers,
|
||||
numSeeds,
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
gameDownloading?.id === game?.id &&
|
||||
GameStatusHelper.isDownloading(game?.status ?? null);
|
||||
const { progress, eta, lastPacket, isGameDeleting } = useDownload();
|
||||
|
||||
const finalDownloadSize = useMemo(() => {
|
||||
if (!game) return "N/A";
|
||||
if (game.fileSize) return formatBytes(game.fileSize);
|
||||
|
||||
if (gameDownloading?.fileSize && isGameDownloading)
|
||||
return formatBytes(gameDownloading.fileSize);
|
||||
if (lastPacket?.game.fileSize && game?.status === "active")
|
||||
return formatBytes(lastPacket?.game.fileSize);
|
||||
|
||||
return game.repack?.fileSize ?? "N/A";
|
||||
}, [game, isGameDownloading, gameDownloading]);
|
||||
}, [game, lastPacket?.game]);
|
||||
|
||||
const isGameDownloading =
|
||||
game?.status === "active" && lastPacket?.game.id === game?.id;
|
||||
|
||||
const getInfo = () => {
|
||||
if (isGameDeleting(game?.id ?? -1)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
}
|
||||
if (isGameDeleting(game?.id ?? -1)) return <p>{t("deleting")}</p>;
|
||||
|
||||
if (game?.progress === 1) return <HeroPanelPlaytime />;
|
||||
|
||||
if (game?.status === "active") {
|
||||
if (lastPacket?.isDownloadingMetadata && isGameDownloading) {
|
||||
return (
|
||||
<>
|
||||
<p>{progress}</p>
|
||||
<p>{t("downloading_metadata")}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const sizeDownloaded = formatBytes(
|
||||
lastPacket?.game?.bytesDownloaded ?? game?.bytesDownloaded
|
||||
);
|
||||
|
||||
const showPeers =
|
||||
game?.downloader === Downloader.Torrent &&
|
||||
lastPacket?.numPeers !== undefined;
|
||||
|
||||
if (isGameDownloading && gameDownloading?.status) {
|
||||
return (
|
||||
<>
|
||||
<p className={styles.downloadDetailsRow}>
|
||||
{progress}
|
||||
{eta && <small>{t("eta", { eta })}</small>}
|
||||
{isGameDownloading
|
||||
? progress
|
||||
: formatDownloadProgress(game?.progress)}
|
||||
|
||||
<small>{eta ? t("eta", { eta }) : t("calculating_eta")}</small>
|
||||
</p>
|
||||
|
||||
{gameDownloading.status !== GameStatus.Downloading ? (
|
||||
<>
|
||||
<p>{t(gameDownloading.status)}</p>
|
||||
{eta && <small>{t("eta", { eta })}</small>}
|
||||
</>
|
||||
) : (
|
||||
<p className={styles.downloadDetailsRow}>
|
||||
{formatBytes(gameDownloading.bytesDownloaded)} /{" "}
|
||||
{finalDownloadSize}
|
||||
<p className={styles.downloadDetailsRow}>
|
||||
<span>
|
||||
{sizeDownloaded} / {finalDownloadSize}
|
||||
</span>
|
||||
{showPeers && (
|
||||
<small>
|
||||
{game?.downloader === Downloader.Torrent &&
|
||||
`${numPeers} peers / ${numSeeds} seeds`}
|
||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.Paused) {
|
||||
if (game?.status === "paused") {
|
||||
const formattedProgress = formatDownloadProgress(game.progress);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{t("paused_progress", {
|
||||
progress: formatDownloadProgress(game.progress),
|
||||
})}
|
||||
<p className={styles.downloadDetailsRow}>
|
||||
{formattedProgress} <small>{t("paused")}</small>
|
||||
</p>
|
||||
<p>
|
||||
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
||||
@ -108,10 +95,6 @@ export function HeroPanel({
|
||||
);
|
||||
}
|
||||
|
||||
if (game && GameStatusHelper.isReady(game?.status ?? GameStatus.Finished)) {
|
||||
return <HeroPanelPlaytime game={game} isGamePlaying={isGamePlaying} />;
|
||||
}
|
||||
|
||||
const [latestRepack] = repacks;
|
||||
|
||||
if (latestRepack) {
|
||||
@ -129,28 +112,33 @@ export function HeroPanel({
|
||||
return <p>{t("no_downloads")}</p>;
|
||||
};
|
||||
|
||||
const backgroundColor = gameColor
|
||||
? (new Color(gameColor).darken(0.6).toString() as string)
|
||||
: "";
|
||||
|
||||
const showProgressBar =
|
||||
(game?.status === "active" && game?.progress < 1) ||
|
||||
game?.status === "paused";
|
||||
|
||||
return (
|
||||
<>
|
||||
<BinaryNotFoundModal
|
||||
visible={showBinaryNotFoundModal}
|
||||
onClose={() => setShowBinaryNotFoundModal(false)}
|
||||
/>
|
||||
|
||||
<div style={{ backgroundColor: color }} className={styles.panel}>
|
||||
<div style={{ backgroundColor }} className={styles.panel}>
|
||||
<div className={styles.content}>{getInfo()}</div>
|
||||
<div className={styles.actions}>
|
||||
<HeroPanelActions
|
||||
game={game}
|
||||
repacks={repacks}
|
||||
objectID={objectID}
|
||||
title={title}
|
||||
getGame={getGame}
|
||||
openRepacksModal={openRepacksModal}
|
||||
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
|
||||
isGamePlaying={isGamePlaying}
|
||||
isGameDownloading={isGameDownloading}
|
||||
/>
|
||||
<HeroPanelActions />
|
||||
</div>
|
||||
|
||||
{showProgressBar && (
|
||||
<progress
|
||||
max={1}
|
||||
value={
|
||||
isGameDownloading ? lastPacket?.game.progress : game?.progress
|
||||
}
|
||||
className={styles.progressBar({
|
||||
disabled: game?.status === "paused",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const container = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const downloadsPathField = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const hintText = style({
|
||||
fontSize: "12px",
|
||||
color: vars.color.body,
|
||||
});
|
||||
|
||||
export const downloaders = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const downloaderOption = style({
|
||||
flex: "1",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const downloaderIcon = style({
|
||||
position: "absolute",
|
||||
left: `${SPACING_UNIT * 2}px`,
|
||||
});
|
@ -0,0 +1,177 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { DiskSpace } from "check-disk-space";
|
||||
import * as styles from "./download-settings-modal.css";
|
||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
|
||||
import type { GameRepack } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
|
||||
export interface DownloadSettingsModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
startDownload: (
|
||||
repack: GameRepack,
|
||||
downloader: Downloader,
|
||||
downloadPath: string
|
||||
) => Promise<void>;
|
||||
repack: GameRepack | null;
|
||||
}
|
||||
|
||||
const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
|
||||
|
||||
export function DownloadSettingsModal({
|
||||
visible,
|
||||
onClose,
|
||||
startDownload,
|
||||
repack,
|
||||
}: DownloadSettingsModalProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
|
||||
const [selectedPath, setSelectedPath] = useState("");
|
||||
const [downloadStarting, setDownloadStarting] = useState(false);
|
||||
const [selectedDownloader, setSelectedDownloader] = useState(
|
||||
Downloader.Torrent
|
||||
);
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
getDiskFreeSpace(selectedPath);
|
||||
}
|
||||
}, [visible, selectedPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userPreferences?.downloadsPath) {
|
||||
setSelectedPath(userPreferences.downloadsPath);
|
||||
} else {
|
||||
window.electron
|
||||
.getDefaultDownloadsPath()
|
||||
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
|
||||
}
|
||||
|
||||
if (userPreferences?.realDebridApiToken)
|
||||
setSelectedDownloader(Downloader.RealDebrid);
|
||||
}, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]);
|
||||
|
||||
const getDiskFreeSpace = (path: string) => {
|
||||
window.electron.getDiskFreeSpace(path).then((result) => {
|
||||
setDiskFreeSpace(result);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChooseDownloadsPath = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
defaultPath: selectedPath,
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
setSelectedPath(path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartClick = () => {
|
||||
if (repack) {
|
||||
setDownloadStarting(true);
|
||||
|
||||
startDownload(repack, selectedDownloader, selectedPath).finally(() => {
|
||||
setDownloadStarting(false);
|
||||
onClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("download_settings")}
|
||||
description={t("space_left_on_disk", {
|
||||
space: formatBytes(diskFreeSpace?.free ?? 0),
|
||||
})}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
marginBottom: `${SPACING_UNIT}px`,
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{t("downloader")}
|
||||
</span>
|
||||
|
||||
<div className={styles.downloaders}>
|
||||
{downloaders.map((downloader) => (
|
||||
<Button
|
||||
key={downloader}
|
||||
className={styles.downloaderOption}
|
||||
theme={
|
||||
selectedDownloader === downloader ? "primary" : "outline"
|
||||
}
|
||||
disabled={
|
||||
downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken
|
||||
}
|
||||
onClick={() => setSelectedDownloader(downloader)}
|
||||
>
|
||||
{selectedDownloader === downloader && (
|
||||
<CheckCircleFillIcon className={styles.downloaderIcon} />
|
||||
)}
|
||||
{DOWNLOADER_NAME[downloader]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div className={styles.downloadsPathField}>
|
||||
<TextField
|
||||
value={selectedPath}
|
||||
readOnly
|
||||
disabled
|
||||
label={t("download_path")}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
theme="outline"
|
||||
onClick={handleChooseDownloadsPath}
|
||||
disabled={downloadStarting}
|
||||
>
|
||||
{t("change")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className={styles.hintText}>
|
||||
<Trans i18nKey="select_folder_hint" ns="game_details">
|
||||
<Link to="/settings" />
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleStartClick} disabled={downloadStarting}>
|
||||
<DownloadIcon />
|
||||
{t("download_now")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
3
src/renderer/src/pages/game-details/modals/index.ts
Normal file
3
src/renderer/src/pages/game-details/modals/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./installation-guides";
|
||||
export * from "./repacks-modal";
|
||||
export * from "./download-settings-modal";
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user