mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-15 04:32:13 +00:00
commit
ddd9ea69df
126 changed files with 2248 additions and 2226 deletions
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
|
@ -22,17 +22,6 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
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
|
- name: Build Linux
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: yarn build:linux
|
run: yarn build:linux
|
||||||
|
|
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
|
@ -24,17 +24,6 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
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
|
- name: Build Linux
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: yarn build:linux
|
run: yarn build:linux
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
||||||
.vscode
|
.vscode
|
||||||
node_modules
|
node_modules
|
||||||
hydra-download-manager
|
aria2/
|
||||||
fastlist.exe
|
fastlist.exe
|
||||||
__pycache__
|
__pycache__
|
||||||
dist
|
dist
|
||||||
|
|
|
@ -3,10 +3,10 @@ productName: Hydra
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
extraResources:
|
extraResources:
|
||||||
- hydra-download-manager
|
- aria2
|
||||||
|
- seeds
|
||||||
- hydra.db
|
- hydra.db
|
||||||
- fastlist.exe
|
- fastlist.exe
|
||||||
- seeds
|
|
||||||
files:
|
files:
|
||||||
- "!**/.vscode/*"
|
- "!**/.vscode/*"
|
||||||
- "!src/*"
|
- "!src/*"
|
||||||
|
|
BIN
hydra.db
BIN
hydra.db
Binary file not shown.
|
@ -4,7 +4,6 @@
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
"homepage": "https://hydralauncher.site",
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/hydralauncher/hydra.git"
|
"url": "https://github.com/hydralauncher/hydra.git"
|
||||||
|
@ -41,6 +40,7 @@
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
"@vanilla-extract/recipes": "^0.5.2",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
|
"aria2": "^4.1.2",
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"better-sqlite3": "^9.5.0",
|
"better-sqlite3": "^9.5.0",
|
||||||
|
@ -49,7 +49,6 @@
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"easydl": "^1.1.1",
|
|
||||||
"electron-log": "^5.1.4",
|
"electron-log": "^5.1.4",
|
||||||
"electron-updater": "^6.1.8",
|
"electron-updater": "^6.1.8",
|
||||||
"fetch-cookie": "^3.0.1",
|
"fetch-cookie": "^3.0.1",
|
||||||
|
@ -59,7 +58,6 @@
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"node-7z-archive": "^1.1.7",
|
|
||||||
"parse-torrent": "^11.0.16",
|
"parse-torrent": "^11.0.16",
|
||||||
"ps-list": "^8.1.1",
|
"ps-list": "^8.1.1",
|
||||||
"react-i18next": "^14.1.0",
|
"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") {
|
if (process.platform === "win32") {
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
|
@ -6,3 +53,5 @@ if (process.platform === "win32") {
|
||||||
"fastlist.exe"
|
"fastlist.exe"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadAria2();
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "إعدادات",
|
"settings": "إعدادات",
|
||||||
"my_library": "مكتبتي",
|
"my_library": "مكتبتي",
|
||||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - جارٍ التحقق من الملفات...)",
|
|
||||||
"paused": "{{title}} (متوقف)",
|
"paused": "{{title}} (متوقف)",
|
||||||
"downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)",
|
"downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)",
|
||||||
"filter": "بحث في المكتبة",
|
"filter": "بحث في المكتبة",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "لا يوجد تنزيلات جارية",
|
"no_downloads_in_progress": "لا يوجد تنزيلات جارية",
|
||||||
"downloading_metadata": "جارٍ تنزيل بيانات وصف {{title}}",
|
"downloading_metadata": "جارٍ تنزيل بيانات وصف {{title}}",
|
||||||
"checking_files": "جارٍ التحقق من ملفات {{title}}… ({{percentage}} مكتملة)",
|
|
||||||
"downloading": "جارٍ تنزيل {{title}}… ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}"
|
"downloading": "جارٍ تنزيل {{title}}… ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} متبقية على القرص",
|
"space_left_on_disk": "{{space}} متبقية على القرص",
|
||||||
"eta": "الوقت المتبقي {{eta}}",
|
"eta": "الوقت المتبقي {{eta}}",
|
||||||
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||||
"checking_files": "جاري التحقق من الملفات...",
|
|
||||||
"filter": "تصفية حزم إعادة التجميع",
|
"filter": "تصفية حزم إعادة التجميع",
|
||||||
"requirements": "متطلبات النظام",
|
"requirements": "متطلبات النظام",
|
||||||
"minimum": "الحد الأدنى",
|
"minimum": "الحد الأدنى",
|
||||||
"recommended": "موصى به",
|
"recommended": "موصى به",
|
||||||
"no_minimum_requirements": "{{title}} لا تتوفر معلومات عن الحد الأدنى للمتطلبات",
|
"no_minimum_requirements": "{{title}} لا تتوفر معلومات عن الحد الأدنى للمتطلبات",
|
||||||
"no_recommended_requirements": "{{title}} لا تتوفر معلومات عن المتطلبات الموصى بها",
|
"no_recommended_requirements": "{{title}} لا تتوفر معلومات عن المتطلبات الموصى بها",
|
||||||
"paused_progress": "{{progress}} (متوقف)",
|
|
||||||
"release_date": "تم الإصدار في {{date}}",
|
"release_date": "تم الإصدار في {{date}}",
|
||||||
"publisher": "نشر بواسطة {{publisher}}",
|
"publisher": "نشر بواسطة {{publisher}}",
|
||||||
"copy_link_to_clipboard": "نسخ الرابط",
|
"copy_link_to_clipboard": "نسخ الرابط",
|
||||||
|
@ -120,22 +116,18 @@
|
||||||
"verifying": "جار التحقق…",
|
"verifying": "جار التحقق…",
|
||||||
"completed_at": "اكتمل في {{date}}",
|
"completed_at": "اكتمل في {{date}}",
|
||||||
"completed": "اكتمل",
|
"completed": "اكتمل",
|
||||||
"cancelled": "ملغي",
|
|
||||||
"download_again": "تحميل مرة أخرى",
|
"download_again": "تحميل مرة أخرى",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"filter": "تصفية الألعاب التي تم تنزيلها",
|
"filter": "تصفية الألعاب التي تم تنزيلها",
|
||||||
"remove": "إزالة",
|
"remove": "إزالة",
|
||||||
"downloading_metadata": "جار تنزيل البيانات الوصفية…",
|
"downloading_metadata": "جار تنزيل البيانات الوصفية…",
|
||||||
"checking_files": "جار التحقق من الملفات…",
|
|
||||||
"starting_download": "يبدأ التنزيل…",
|
"starting_download": "يبدأ التنزيل…",
|
||||||
"deleting": "جار حذف المثبت…",
|
"deleting": "جار حذف المثبت…",
|
||||||
"delete": "إزالة المثبت",
|
"delete": "إزالة المثبت",
|
||||||
"remove_from_list": "إزالة",
|
"remove_from_list": "إزالة",
|
||||||
"delete_modal_title": "هل أنت متأكد؟",
|
"delete_modal_title": "هل أنت متأكد؟",
|
||||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
|
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
|
||||||
"install": "تثبيت",
|
"install": "تثبيت"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "تورنت"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "مسار التنزيلات",
|
"downloads_path": "مسار التنزيلات",
|
||||||
|
@ -145,14 +137,13 @@
|
||||||
"enable_repack_list_notifications": "عند إضافة حزمة جديدة",
|
"enable_repack_list_notifications": "عند إضافة حزمة جديدة",
|
||||||
"telemetry": "القياس عن بعد",
|
"telemetry": "القياس عن بعد",
|
||||||
"telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر",
|
"telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر",
|
||||||
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal Debrid ",
|
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal-Debrid ",
|
||||||
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
|
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
|
||||||
"launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام",
|
"launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام",
|
||||||
"general": "عام",
|
"general": "عام",
|
||||||
"behavior": "السلوك",
|
"behavior": "السلوك",
|
||||||
"enable_real_debrid": "تفعيل Real Debrid ",
|
"enable_real_debrid": "تفعيل Real-Debrid ",
|
||||||
"real_debrid": "Real Debrid",
|
"real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا",
|
||||||
"real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا.",
|
|
||||||
"save_changes": "حفظ التغييرات"
|
"save_changes": "حفظ التغييرات"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Налады",
|
"settings": "Налады",
|
||||||
"my_library": "Мая бібліятэка",
|
"my_library": "Мая бібліятэка",
|
||||||
"downloading_metadata": "{{title}} (Сцягванне мэтаданых…)",
|
"downloading_metadata": "{{title}} (Сцягванне мэтаданых…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Праверка файлаў…)",
|
|
||||||
"paused": "{{title}} (Спынена)",
|
"paused": "{{title}} (Спынена)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
||||||
"filter": "Фільтар бібліятэкі",
|
"filter": "Фільтар бібліятэкі",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Няма актыўных сцягванняў",
|
"no_downloads_in_progress": "Няма актыўных сцягванняў",
|
||||||
"downloading_metadata": "Сцягванне мэтаданых {{title}}…",
|
"downloading_metadata": "Сцягванне мэтаданых {{title}}…",
|
||||||
"checking_files": "Праверка файлаў {{title}}… ({{percentage}} скончана)",
|
|
||||||
"downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}"
|
"downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} засталося на дыску",
|
"space_left_on_disk": "{{space}} засталося на дыску",
|
||||||
"eta": "Канчатак {{eta}}",
|
"eta": "Канчатак {{eta}}",
|
||||||
"downloading_metadata": "Сцягванне мэтаданых…",
|
"downloading_metadata": "Сцягванне мэтаданых…",
|
||||||
"checking_files": "Праверка файлаў…",
|
|
||||||
"filter": "Фільтар рэпакаў",
|
"filter": "Фільтар рэпакаў",
|
||||||
"requirements": "Сістэмныя патрэбаванни",
|
"requirements": "Сістэмныя патрэбаванни",
|
||||||
"minimum": "Мінімальныя",
|
"minimum": "Мінімальныя",
|
||||||
"recommended": "Рэкамендуемыя",
|
"recommended": "Рэкамендуемыя",
|
||||||
"no_minimum_requirements": "{{title}} ня ўтрымлівае інфармацыі пра мінімальныя патрабаванні",
|
"no_minimum_requirements": "{{title}} ня ўтрымлівае інфармацыі пра мінімальныя патрабаванні",
|
||||||
"no_recommended_requirements": "{{title}} ня ўтрымлівае інфармацыі пра рэкамендуемыя патрабаванні",
|
"no_recommended_requirements": "{{title}} ня ўтрымлівае інфармацыі пра рэкамендуемыя патрабаванні",
|
||||||
"paused_progress": "{{progress}} (Спынена)",
|
|
||||||
"release_date": "Выпушчана {{date}}",
|
"release_date": "Выпушчана {{date}}",
|
||||||
"publisher": "Выдана {{publisher}}",
|
"publisher": "Выдана {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Скапіяваць спасылку",
|
"copy_link_to_clipboard": "Скапіяваць спасылку",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "Зараз гуляе",
|
"playing_now": "Зараз гуляе",
|
||||||
"change": "Змяніць",
|
"change": "Змяніць",
|
||||||
"repacks_modal_description": "Абярыце рэпак, які хочаце сцягнуць",
|
"repacks_modal_description": "Абярыце рэпак, які хочаце сцягнуць",
|
||||||
"downloads_path": "Шлях сцягвання",
|
|
||||||
"select_folder_hint": "Каб змяніць папку па змоўчанні, адкрыйце",
|
"select_folder_hint": "Каб змяніць папку па змоўчанні, адкрыйце",
|
||||||
"download_now": "Сцягнуць зараз",
|
"download_now": "Сцягнуць зараз",
|
||||||
"installation_instructions": "Інструкцыя ўсталёўкі",
|
"installation_instructions": "Інструкцыя ўсталёўкі",
|
||||||
|
@ -114,13 +109,11 @@
|
||||||
"verifying": "Праверка…",
|
"verifying": "Праверка…",
|
||||||
"completed_at": "Скончана а {{date}}",
|
"completed_at": "Скончана а {{date}}",
|
||||||
"completed": "Скончана",
|
"completed": "Скончана",
|
||||||
"cancelled": "Скасавана",
|
|
||||||
"download_again": "Сцягнуць зноў",
|
"download_again": "Сцягнуць зноў",
|
||||||
"cancel": "Скасаваць",
|
"cancel": "Скасаваць",
|
||||||
"filter": "Фільтар сцягнутых гульняў",
|
"filter": "Фільтар сцягнутых гульняў",
|
||||||
"remove": "Выдаліць",
|
"remove": "Выдаліць",
|
||||||
"downloading_metadata": "Сцягванне мэтаданых…",
|
"downloading_metadata": "Сцягванне мэтаданых…",
|
||||||
"checking_files": "Праверка файлаў…",
|
|
||||||
"starting_download": "Пачатак сцягвання…",
|
"starting_download": "Пачатак сцягвання…",
|
||||||
"deleting": "Выдаленне ўсталёўшчыка…",
|
"deleting": "Выдаленне ўсталёўшчыка…",
|
||||||
"delete": "Выдаліць усталёўшчык",
|
"delete": "Выдаліць усталёўшчык",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Indstillinger",
|
"settings": "Indstillinger",
|
||||||
"my_library": "Mit bibliotek",
|
"my_library": "Mit bibliotek",
|
||||||
"downloading_metadata": "{{title}} (Downloader metadata…)",
|
"downloading_metadata": "{{title}} (Downloader metadata…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Tjekker filer…)",
|
|
||||||
"paused": "{{title}} (Paused)",
|
"paused": "{{title}} (Paused)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filtrer bibliotek",
|
"filter": "Filtrer bibliotek",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Ingen downloads igang",
|
"no_downloads_in_progress": "Ingen downloads igang",
|
||||||
"downloading_metadata": "Downloader {{title}} metadata…",
|
"downloading_metadata": "Downloader {{title}} metadata…",
|
||||||
"checking_files": "Tjekker {{title}} filer… ({{percentage}} færdig)",
|
|
||||||
"downloading": "Downloader {{title}}… ({{percentage}} færdig) - Konklusion {{eta}} - {{speed}}"
|
"downloading": "Downloader {{title}}… ({{percentage}} færdig) - Konklusion {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} tilbage på harddisken",
|
"space_left_on_disk": "{{space}} tilbage på harddisken",
|
||||||
"eta": "Konklusion {{eta}}",
|
"eta": "Konklusion {{eta}}",
|
||||||
"downloading_metadata": "Downloader metadata…",
|
"downloading_metadata": "Downloader metadata…",
|
||||||
"checking_files": "Tjekker filer…",
|
|
||||||
"filter": "Filtrer repacks",
|
"filter": "Filtrer repacks",
|
||||||
"requirements": "System behov",
|
"requirements": "System behov",
|
||||||
"minimum": "Mindste",
|
"minimum": "Mindste",
|
||||||
"recommended": "Anbefalet",
|
"recommended": "Anbefalet",
|
||||||
"no_minimum_requirements": "{{title}} angiver ikke mindste behov informationer",
|
"no_minimum_requirements": "{{title}} angiver ikke mindste behov informationer",
|
||||||
"no_recommended_requirements": "{{title}} angiver ikke anbefalet behov informationer",
|
"no_recommended_requirements": "{{title}} angiver ikke anbefalet behov informationer",
|
||||||
"paused_progress": "{{progress}} (Pauset)",
|
|
||||||
"release_date": "Offentliggjort den {{date}}",
|
"release_date": "Offentliggjort den {{date}}",
|
||||||
"publisher": "Udgivet af {{publisher}}",
|
"publisher": "Udgivet af {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Kopier link",
|
"copy_link_to_clipboard": "Kopier link",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "Spiller nu",
|
"playing_now": "Spiller nu",
|
||||||
"change": "Ændré",
|
"change": "Ændré",
|
||||||
"repacks_modal_description": "Vælg den repack du vil downloade",
|
"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>",
|
"select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne</0>",
|
||||||
"download_now": "Download nu",
|
"download_now": "Download nu",
|
||||||
"installation_instructions": "Installations Instrukser",
|
"installation_instructions": "Installations Instrukser",
|
||||||
|
@ -114,22 +109,18 @@
|
||||||
"verifying": "Verificerer…",
|
"verifying": "Verificerer…",
|
||||||
"completed_at": "Færdiggjort på {{date}}",
|
"completed_at": "Færdiggjort på {{date}}",
|
||||||
"completed": "Færdigt",
|
"completed": "Færdigt",
|
||||||
"cancelled": "Annulleret",
|
|
||||||
"download_again": "Download igen",
|
"download_again": "Download igen",
|
||||||
"cancel": "Annullér",
|
"cancel": "Annullér",
|
||||||
"filter": "Filtrer downloadet spil",
|
"filter": "Filtrer downloadet spil",
|
||||||
"remove": "Fjern",
|
"remove": "Fjern",
|
||||||
"downloading_metadata": "Downloader metadata…",
|
"downloading_metadata": "Downloader metadata…",
|
||||||
"checking_files": "Tjekker filer…",
|
|
||||||
"starting_download": "Starter download…",
|
"starting_download": "Starter download…",
|
||||||
"deleting": "Sletter installatør…",
|
"deleting": "Sletter installatør…",
|
||||||
"delete": "Fjern installatør",
|
"delete": "Fjern installatør",
|
||||||
"remove_from_list": "Fjern",
|
"remove_from_list": "Fjern",
|
||||||
"delete_modal_title": "Er du sikker?",
|
"delete_modal_title": "Er du sikker?",
|
||||||
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
|
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
|
||||||
"install": "Installér",
|
"install": "Installér"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloads sti",
|
"downloads_path": "Downloads sti",
|
||||||
|
@ -139,14 +130,12 @@
|
||||||
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
|
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
|
||||||
"telemetry": "Telemetri",
|
"telemetry": "Telemetri",
|
||||||
"telemetry_description": "Slå anonymt brugs statistik til",
|
"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",
|
"quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen",
|
||||||
"launch_with_system": "Åben Hydra ved start af systemet",
|
"launch_with_system": "Åben Hydra ved start af systemet",
|
||||||
"general": "Generelt",
|
"general": "Generelt",
|
||||||
"behavior": "Opførsel",
|
"behavior": "Opførsel",
|
||||||
"enable_real_debrid": "Slå Real Debrid til",
|
"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>",
|
||||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>.",
|
|
||||||
"save_changes": "Gem ændringer"
|
"save_changes": "Gem ændringer"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"my_library": "My library",
|
"my_library": "My library",
|
||||||
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Checking files…)",
|
|
||||||
"paused": "{{title}} (Paused)",
|
"paused": "{{title}} (Paused)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filter library",
|
"filter": "Filter library",
|
||||||
|
@ -35,8 +34,8 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "No downloads in progress",
|
"no_downloads_in_progress": "No downloads in progress",
|
||||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
"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": {
|
"catalogue": {
|
||||||
"next_page": "Next page",
|
"next_page": "Next page",
|
||||||
|
@ -56,15 +55,15 @@
|
||||||
"remove_from_list": "Remove",
|
"remove_from_list": "Remove",
|
||||||
"space_left_on_disk": "{{space}} left on disk",
|
"space_left_on_disk": "{{space}} left on disk",
|
||||||
"eta": "Conclusion {{eta}}",
|
"eta": "Conclusion {{eta}}",
|
||||||
|
"calculating_eta": "Calculating remaining time…",
|
||||||
"downloading_metadata": "Downloading metadata…",
|
"downloading_metadata": "Downloading metadata…",
|
||||||
"checking_files": "Checking files…",
|
|
||||||
"filter": "Filter repacks",
|
"filter": "Filter repacks",
|
||||||
"requirements": "System requirements",
|
"requirements": "System requirements",
|
||||||
"minimum": "Minimum",
|
"minimum": "Minimum",
|
||||||
"recommended": "Recommended",
|
"recommended": "Recommended",
|
||||||
"no_minimum_requirements": "{{title}} doesn't provide minimum requirements information",
|
"no_minimum_requirements": "{{title}} doesn't provide minimum requirements information",
|
||||||
"no_recommended_requirements": "{{title}} doesn't provide recommended requirements information",
|
"no_recommended_requirements": "{{title}} doesn't provide recommended requirements information",
|
||||||
"paused_progress": "{{progress}} (Paused)",
|
"paused": "Paused",
|
||||||
"release_date": "Released on {{date}}",
|
"release_date": "Released on {{date}}",
|
||||||
"publisher": "Published by {{publisher}}",
|
"publisher": "Published by {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Copy link",
|
"copy_link_to_clipboard": "Copy link",
|
||||||
|
@ -103,7 +102,9 @@
|
||||||
"previous_screenshot": "Previous screenshot",
|
"previous_screenshot": "Previous screenshot",
|
||||||
"next_screenshot": "Next screenshot",
|
"next_screenshot": "Next screenshot",
|
||||||
"screenshot": "Screenshot {{number}}",
|
"screenshot": "Screenshot {{number}}",
|
||||||
"open_screenshot": "Open screenshot {{number}}"
|
"open_screenshot": "Open screenshot {{number}}",
|
||||||
|
"download_settings": "Download settings",
|
||||||
|
"downloader": "Downloader"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -121,22 +122,19 @@
|
||||||
"verifying": "Verifying…",
|
"verifying": "Verifying…",
|
||||||
"completed_at": "Completed in {{date}}",
|
"completed_at": "Completed in {{date}}",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"cancelled": "Cancelled",
|
"removed": "Not downloaded",
|
||||||
"download_again": "Download again",
|
"download_again": "Download again",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"filter": "Filter downloaded games",
|
"filter": "Filter downloaded games",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"downloading_metadata": "Downloading metadata…",
|
"downloading_metadata": "Downloading metadata…",
|
||||||
"checking_files": "Checking files…",
|
|
||||||
"starting_download": "Starting download…",
|
"starting_download": "Starting download…",
|
||||||
"deleting": "Deleting installer…",
|
"deleting": "Deleting installer…",
|
||||||
"delete": "Remove installer",
|
"delete": "Remove installer",
|
||||||
"remove_from_list": "Remove",
|
"remove_from_list": "Remove",
|
||||||
"delete_modal_title": "Are you sure?",
|
"delete_modal_title": "Are you sure?",
|
||||||
"delete_modal_description": "This will remove all the installation files from your computer",
|
"delete_modal_description": "This will remove all the installation files from your computer",
|
||||||
"install": "Install",
|
"install": "Install"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloads path",
|
"downloads_path": "Downloads path",
|
||||||
|
@ -146,15 +144,20 @@
|
||||||
"enable_repack_list_notifications": "When a new repack is added",
|
"enable_repack_list_notifications": "When a new repack is added",
|
||||||
"telemetry": "Telemetry",
|
"telemetry": "Telemetry",
|
||||||
"telemetry_description": "Enable anonymous usage statistics",
|
"telemetry_description": "Enable anonymous usage statistics",
|
||||||
"real_debrid_api_token_label": "Real Debrid API token",
|
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||||
"quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray",
|
"quit_app_instead_hiding": "Don't hide Hydra when closing",
|
||||||
"launch_with_system": "Launch Hydra on system start-up",
|
"launch_with_system": "Launch Hydra on system start-up",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Behavior",
|
"behavior": "Behavior",
|
||||||
"enable_real_debrid": "Enable Real Debrid",
|
"real_debrid_api_token": "API Token",
|
||||||
"real_debrid": "Real Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "You can get your API key <0>here</0>.",
|
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.",
|
||||||
"save_changes": "Save changes"
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"my_library": "Mi biblioteca",
|
"my_library": "Mi biblioteca",
|
||||||
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Analizando archivos…)",
|
|
||||||
"paused": "{{title}} (Pausado)",
|
"paused": "{{title}} (Pausado)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
||||||
"filter": "Buscar en la biblioteca",
|
"filter": "Buscar en la biblioteca",
|
||||||
|
@ -35,7 +34,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Sin descargas en progreso",
|
"no_downloads_in_progress": "Sin descargas en progreso",
|
||||||
"downloading_metadata": "Descargando metadatos de {{title}}…",
|
"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}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -57,14 +55,12 @@
|
||||||
"space_left_on_disk": "{{space}} restantes en el disco",
|
"space_left_on_disk": "{{space}} restantes en el disco",
|
||||||
"eta": "Tiempo restante: {{eta}}",
|
"eta": "Tiempo restante: {{eta}}",
|
||||||
"downloading_metadata": "Descargando metadatos…",
|
"downloading_metadata": "Descargando metadatos…",
|
||||||
"checking_files": "Analizando archivos…",
|
|
||||||
"filter": "Buscar repacks",
|
"filter": "Buscar repacks",
|
||||||
"requirements": "Requisitos del Sistema",
|
"requirements": "Requisitos del Sistema",
|
||||||
"minimum": "Mínimos",
|
"minimum": "Mínimos",
|
||||||
"recommended": "Recomendados",
|
"recommended": "Recomendados",
|
||||||
"no_minimum_requirements": "Sin requisitos mínimos para {{title}}",
|
"no_minimum_requirements": "Sin requisitos mínimos para {{title}}",
|
||||||
"no_recommended_requirements": "{{title}} no tiene requisitos recomendados",
|
"no_recommended_requirements": "{{title}} no tiene requisitos recomendados",
|
||||||
"paused_progress": "{{progress}} (Pausado)",
|
|
||||||
"release_date": "Fecha de lanzamiento: {{date}}",
|
"release_date": "Fecha de lanzamiento: {{date}}",
|
||||||
"publisher": "Publicado por: {{publisher}}",
|
"publisher": "Publicado por: {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Copiar enlace",
|
"copy_link_to_clipboard": "Copiar enlace",
|
||||||
|
@ -121,22 +117,18 @@
|
||||||
"verifying": "Verificando…",
|
"verifying": "Verificando…",
|
||||||
"completed_at": "Completado el {{date}}",
|
"completed_at": "Completado el {{date}}",
|
||||||
"completed": "Completado",
|
"completed": "Completado",
|
||||||
"cancelled": "Cancelado",
|
|
||||||
"download_again": "Descargar de nuevo",
|
"download_again": "Descargar de nuevo",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"filter": "Buscar juegos descargados",
|
"filter": "Buscar juegos descargados",
|
||||||
"remove": "Eliminar",
|
"remove": "Eliminar",
|
||||||
"downloading_metadata": "Descargando metadatos…",
|
"downloading_metadata": "Descargando metadatos…",
|
||||||
"checking_files": "Verificando archivos…",
|
|
||||||
"starting_download": "Iniciando descarga…",
|
"starting_download": "Iniciando descarga…",
|
||||||
"deleting": "Eliminando instalador…",
|
"deleting": "Eliminando instalador…",
|
||||||
"delete": "Eliminar instalador",
|
"delete": "Eliminar instalador",
|
||||||
"remove_from_list": "Eliminar",
|
"remove_from_list": "Eliminar",
|
||||||
"delete_modal_title": "¿Estás seguro?",
|
"delete_modal_title": "¿Estás seguro?",
|
||||||
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
|
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
|
||||||
"install": "Instalar",
|
"install": "Instalar"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Ruta de descarga",
|
"downloads_path": "Ruta de descarga",
|
||||||
|
@ -146,14 +138,13 @@
|
||||||
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
||||||
"telemetry": "Telemetría",
|
"telemetry": "Telemetría",
|
||||||
"telemetry_description": "Habilitar recopilación de datos de manera anónima",
|
"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",
|
"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",
|
"launch_with_system": "Iniciar Hydra al inicio del sistema",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Otros",
|
"behavior": "Otros",
|
||||||
"enable_real_debrid": "Activar Real Debrid",
|
"enable_real_debrid": "Activar Real-Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
||||||
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>.",
|
|
||||||
"save_changes": "Guardar cambios"
|
"save_changes": "Guardar cambios"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "تنظیمات",
|
"settings": "تنظیمات",
|
||||||
"my_library": "کتابخانهی من",
|
"my_library": "کتابخانهی من",
|
||||||
"downloading_metadata": "{{title}} (در حال دانلود متادیتا...)",
|
"downloading_metadata": "{{title}} (در حال دانلود متادیتا...)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - در حال بررسی فایلها...)",
|
|
||||||
"paused": "{{title}} (متوقف شده)",
|
"paused": "{{title}} (متوقف شده)",
|
||||||
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
||||||
"filter": "فیلتر کردن کتابخانه",
|
"filter": "فیلتر کردن کتابخانه",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "دانلودی در حال انجام نیست",
|
"no_downloads_in_progress": "دانلودی در حال انجام نیست",
|
||||||
"downloading_metadata": "درحال دانلود متادیتاهای {{title}}…",
|
"downloading_metadata": "درحال دانلود متادیتاهای {{title}}…",
|
||||||
"checking_files": "در حال چک کردن فایلهای {{title}}…. ({{percentage}} تکمیل شده)",
|
|
||||||
"downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}"
|
"downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} فضا در دیسک باقیمانده",
|
"space_left_on_disk": "{{space}} فضا در دیسک باقیمانده",
|
||||||
"eta": "اتمام {{eta}}",
|
"eta": "اتمام {{eta}}",
|
||||||
"downloading_metadata": "در حال دانلود متادیتاها…",
|
"downloading_metadata": "در حال دانلود متادیتاها…",
|
||||||
"checking_files": "در حال چک کردن فایلها",
|
|
||||||
"filter": "فیلترکردن ریپکها",
|
"filter": "فیلترکردن ریپکها",
|
||||||
"requirements": "سیستم مورد نیاز",
|
"requirements": "سیستم مورد نیاز",
|
||||||
"minimum": "حداقل",
|
"minimum": "حداقل",
|
||||||
"recommended": "پیشنهادی",
|
"recommended": "پیشنهادی",
|
||||||
"no_minimum_requirements": "{{title}} اطلاعات حداقل سیستم مورد نیاز را فراهم نکرده",
|
"no_minimum_requirements": "{{title}} اطلاعات حداقل سیستم مورد نیاز را فراهم نکرده",
|
||||||
"no_recommended_requirements": "{{title}} اطلاعات پیشنهادی سیستم مورد نیاز را فراهم نکرده",
|
"no_recommended_requirements": "{{title}} اطلاعات پیشنهادی سیستم مورد نیاز را فراهم نکرده",
|
||||||
"paused_progress": "{{progress}} (متوقف شده)",
|
|
||||||
"release_date": "منتشر شده در {{date}}",
|
"release_date": "منتشر شده در {{date}}",
|
||||||
"publisher": "منتشر شده توسط {{publisher}}",
|
"publisher": "منتشر شده توسط {{publisher}}",
|
||||||
"copy_link_to_clipboard": "کپی لینک",
|
"copy_link_to_clipboard": "کپی لینک",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "در حال بازی",
|
"playing_now": "در حال بازی",
|
||||||
"change": "تغییر",
|
"change": "تغییر",
|
||||||
"repacks_modal_description": "ریپک مورد نظر برای دانلود را انتخاب کنید",
|
"repacks_modal_description": "ریپک مورد نظر برای دانلود را انتخاب کنید",
|
||||||
"downloads_path": "آدرس دانلودها",
|
|
||||||
"select_folder_hint": "برای تغییر پوشهی پیشفرض به <0>Settings</0> بروید",
|
"select_folder_hint": "برای تغییر پوشهی پیشفرض به <0>Settings</0> بروید",
|
||||||
"download_now": "الان دانلود کن",
|
"download_now": "الان دانلود کن",
|
||||||
"installation_instructions": "دستورات نصب",
|
"installation_instructions": "دستورات نصب",
|
||||||
|
@ -114,22 +109,18 @@
|
||||||
"verifying": "در حال اعتبارسنجی…",
|
"verifying": "در حال اعتبارسنجی…",
|
||||||
"completed_at": "پایان یافته در {{date}}",
|
"completed_at": "پایان یافته در {{date}}",
|
||||||
"completed": "پایان یافته",
|
"completed": "پایان یافته",
|
||||||
"cancelled": "لغو شده",
|
|
||||||
"download_again": "دانلود مجدد",
|
"download_again": "دانلود مجدد",
|
||||||
"cancel": "لغو",
|
"cancel": "لغو",
|
||||||
"filter": "فیلتر بازیهای دانلود شده",
|
"filter": "فیلتر بازیهای دانلود شده",
|
||||||
"remove": "حذف",
|
"remove": "حذف",
|
||||||
"downloading_metadata": "در حال دانلود متادیتاها…",
|
"downloading_metadata": "در حال دانلود متادیتاها…",
|
||||||
"checking_files": "در حال چک کردن فایلها…",
|
|
||||||
"starting_download": "در حال آغار دانلود…",
|
"starting_download": "در حال آغار دانلود…",
|
||||||
"deleting": "در حال پاک کردن اینستالر…",
|
"deleting": "در حال پاک کردن اینستالر…",
|
||||||
"delete": "پاک کردن",
|
"delete": "پاک کردن",
|
||||||
"remove_from_list": "حذف",
|
"remove_from_list": "حذف",
|
||||||
"delete_modal_title": "مطمئنی؟",
|
"delete_modal_title": "مطمئنی؟",
|
||||||
"delete_modal_description": "این کار تمام فایلهای اینستالر را از کامپیوتر شما حذف میکند",
|
"delete_modal_description": "این کار تمام فایلهای اینستالر را از کامپیوتر شما حذف میکند",
|
||||||
"install": "نصف",
|
"install": "نصف"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "تورنت"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "مسیر دانلودها",
|
"downloads_path": "مسیر دانلودها",
|
||||||
|
@ -139,13 +130,11 @@
|
||||||
"enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد",
|
"enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد",
|
||||||
"telemetry": "تلمتری",
|
"telemetry": "تلمتری",
|
||||||
"telemetry_description": "فعال کردن آمارگیری استفاده ناشناس",
|
"telemetry_description": "فعال کردن آمارگیری استفاده ناشناس",
|
||||||
"real_debrid_api_token_description": "توکن Real Debrid",
|
|
||||||
"quit_app_instead_hiding": "به جای کوچک کردن، از هایدرا خارج شو",
|
"quit_app_instead_hiding": "به جای کوچک کردن، از هایدرا خارج شو",
|
||||||
"launch_with_system": "زمانی که سیستم روشن میشود، هایدرا را باز کن",
|
"launch_with_system": "زمانی که سیستم روشن میشود، هایدرا را باز کن",
|
||||||
"general": "کلی",
|
"general": "کلی",
|
||||||
"behavior": "رفتار",
|
"behavior": "رفتار",
|
||||||
"enable_real_debrid": "فعالسازی Real Debrid",
|
"enable_real_debrid": "فعالسازی Real-Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
"real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
||||||
"save_changes": "ذخیره تغییرات"
|
"save_changes": "ذخیره تغییرات"
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"my_library": "Ma bibliothèque",
|
"my_library": "Ma bibliothèque",
|
||||||
"downloading_metadata": "{{title}} (Téléchargement des métadonnées…)",
|
"downloading_metadata": "{{title}} (Téléchargement des métadonnées…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Vérification des fichiers…)",
|
|
||||||
"paused": "{{title}} (En pause)",
|
"paused": "{{title}} (En pause)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||||
"filter": "Filtrer la bibliothèque",
|
"filter": "Filtrer la bibliothèque",
|
||||||
|
@ -30,7 +29,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Aucun téléchargement en cours",
|
"no_downloads_in_progress": "Aucun téléchargement en cours",
|
||||||
"downloading_metadata": "Téléchargement des métadonnées de {{title}}…",
|
"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}}"
|
"downloading": "Téléchargement de {{title}}… ({{percentage}} terminé) - Fin dans {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
|
@ -47,14 +45,12 @@
|
||||||
"space_left_on_disk": "{{space}} restant sur le disque",
|
"space_left_on_disk": "{{space}} restant sur le disque",
|
||||||
"eta": "Fin dans {{eta}}",
|
"eta": "Fin dans {{eta}}",
|
||||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
||||||
"checking_files": "Vérification des fichiers…",
|
|
||||||
"filter": "Filtrer les repacks",
|
"filter": "Filtrer les repacks",
|
||||||
"requirements": "Configuration requise",
|
"requirements": "Configuration requise",
|
||||||
"minimum": "Minimum",
|
"minimum": "Minimum",
|
||||||
"recommended": "Recommandée",
|
"recommended": "Recommandée",
|
||||||
"no_minimum_requirements": "{{title}} ne fournit pas d'informations sur les configurations minimales",
|
"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",
|
"no_recommended_requirements": "{{title}} ne fournit pas d'informations sur les configurations recommandées",
|
||||||
"paused_progress": "{{progress}} (En pause)",
|
|
||||||
"release_date": "Sorti le {{date}}",
|
"release_date": "Sorti le {{date}}",
|
||||||
"publisher": "Édité par {{publisher}}",
|
"publisher": "Édité par {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Copier le lien",
|
"copy_link_to_clipboard": "Copier le lien",
|
||||||
|
@ -93,13 +89,11 @@
|
||||||
"verifying": "Vérification en cours…",
|
"verifying": "Vérification en cours…",
|
||||||
"completed_at": "Terminé en {{date}}",
|
"completed_at": "Terminé en {{date}}",
|
||||||
"completed": "Terminé",
|
"completed": "Terminé",
|
||||||
"cancelled": "Annulé",
|
|
||||||
"download_again": "Télécharger à nouveau",
|
"download_again": "Télécharger à nouveau",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"filter": "Filtrer les jeux téléchargés",
|
"filter": "Filtrer les jeux téléchargés",
|
||||||
"remove": "Supprimer",
|
"remove": "Supprimer",
|
||||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
"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…",
|
"starting_download": "Démarrage du téléchargement…",
|
||||||
"remove_from_list": "Retirer",
|
"remove_from_list": "Retirer",
|
||||||
"delete": "Supprimer le programme d'installation",
|
"delete": "Supprimer le programme d'installation",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Beállítások",
|
"settings": "Beállítások",
|
||||||
"my_library": "Könyvtáram",
|
"my_library": "Könyvtáram",
|
||||||
"downloading_metadata": "{{title}} (Metadata letöltése…)",
|
"downloading_metadata": "{{title}} (Metadata letöltése…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Fájlok ellenőrzése…)",
|
|
||||||
"paused": "{{title}} (Szünet)",
|
"paused": "{{title}} (Szünet)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||||
"filter": "Könyvtár szűrése",
|
"filter": "Könyvtár szűrése",
|
||||||
|
@ -30,7 +29,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
|
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
|
||||||
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
"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}}"
|
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -52,14 +50,12 @@
|
||||||
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
||||||
"eta": "Befejezés {{eta}}",
|
"eta": "Befejezés {{eta}}",
|
||||||
"downloading_metadata": "Metaadatok letöltése…",
|
"downloading_metadata": "Metaadatok letöltése…",
|
||||||
"checking_files": "Fájlok ellenőrzése…",
|
|
||||||
"filter": "Repackek szűrése",
|
"filter": "Repackek szűrése",
|
||||||
"requirements": "Rendszerkövetelmények",
|
"requirements": "Rendszerkövetelmények",
|
||||||
"minimum": "Minimális",
|
"minimum": "Minimális",
|
||||||
"recommended": "Ajánlott",
|
"recommended": "Ajánlott",
|
||||||
"no_minimum_requirements": "{{title}} nem tartalmaz információt a minimális követelményekről",
|
"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",
|
"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}}",
|
"release_date": "Megjelenés: {{date}}",
|
||||||
"publisher": "Kiadta: {{publisher}}",
|
"publisher": "Kiadta: {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Link másolása",
|
"copy_link_to_clipboard": "Link másolása",
|
||||||
|
@ -82,7 +78,6 @@
|
||||||
"playing_now": "Jelenleg játszva",
|
"playing_now": "Jelenleg játszva",
|
||||||
"change": "Változtatás",
|
"change": "Változtatás",
|
||||||
"repacks_modal_description": "Choose the repack you want to download",
|
"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",
|
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
|
||||||
"download_now": "Töltsd le most"
|
"download_now": "Töltsd le most"
|
||||||
},
|
},
|
||||||
|
@ -102,13 +97,11 @@
|
||||||
"verifying": "Ellenőrzés…",
|
"verifying": "Ellenőrzés…",
|
||||||
"completed_at": "Befejezve {{date}}-kor",
|
"completed_at": "Befejezve {{date}}-kor",
|
||||||
"completed": "Befejezve",
|
"completed": "Befejezve",
|
||||||
"cancelled": "Megszakítva",
|
|
||||||
"download_again": "Újra letöltés",
|
"download_again": "Újra letöltés",
|
||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"filter": "Letöltött játékok szűrése",
|
"filter": "Letöltött játékok szűrése",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
"downloading_metadata": "Metaadatok letöltése…",
|
"downloading_metadata": "Metaadatok letöltése…",
|
||||||
"checking_files": "Fájlok ellenőrzése…",
|
|
||||||
"starting_download": "Letöltés indítása…",
|
"starting_download": "Letöltés indítása…",
|
||||||
"deleting": "Telepítő törlése…",
|
"deleting": "Telepítő törlése…",
|
||||||
"delete": "Telepítő eltávolítása",
|
"delete": "Telepítő eltávolítása",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Pengaturan",
|
"settings": "Pengaturan",
|
||||||
"my_library": "Koleksi saya",
|
"my_library": "Koleksi saya",
|
||||||
"downloading_metadata": "{{title}} (Mengunduh metadata…)",
|
"downloading_metadata": "{{title}} (Mengunduh metadata…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Memeriksa file…)",
|
|
||||||
"paused": "{{title}} (Terhenti)",
|
"paused": "{{title}} (Terhenti)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
||||||
"filter": "Filter koleksi",
|
"filter": "Filter koleksi",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Tidak ada unduhan berjalan",
|
"no_downloads_in_progress": "Tidak ada unduhan berjalan",
|
||||||
"downloading_metadata": "Mengunduh metadata {{title}}...",
|
"downloading_metadata": "Mengunduh metadata {{title}}...",
|
||||||
"checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)",
|
|
||||||
"downloading": "Mengunduh {{title}}… ({{percentage}} selesai) - Perkiraan {{eta}} - {{speed}}"
|
"downloading": "Mengunduh {{title}}… ({{percentage}} selesai) - Perkiraan {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} tersisa pada disk",
|
"space_left_on_disk": "{{space}} tersisa pada disk",
|
||||||
"eta": "Perkiraan {{eta}}",
|
"eta": "Perkiraan {{eta}}",
|
||||||
"downloading_metadata": "Mengunduh metadata…",
|
"downloading_metadata": "Mengunduh metadata…",
|
||||||
"checking_files": "Memeriksa file…",
|
|
||||||
"filter": "Saring repacks",
|
"filter": "Saring repacks",
|
||||||
"requirements": "Keperluan sistem",
|
"requirements": "Keperluan sistem",
|
||||||
"minimum": "Minimum",
|
"minimum": "Minimum",
|
||||||
"recommended": "Rekomendasi",
|
"recommended": "Rekomendasi",
|
||||||
"no_minimum_requirements": "{{title}} Tidak ada informasi kebutuhan sistem",
|
"no_minimum_requirements": "{{title}} Tidak ada informasi kebutuhan sistem",
|
||||||
"no_recommended_requirements": "{{title}} Tidak ada informasi rekomendasi kebutuhan sistem",
|
"no_recommended_requirements": "{{title}} Tidak ada informasi rekomendasi kebutuhan sistem",
|
||||||
"paused_progress": "{{progress}} (Terhenti)",
|
|
||||||
"release_date": "Dirilis pada {{date}}",
|
"release_date": "Dirilis pada {{date}}",
|
||||||
"publisher": "Dipublikasikan oleh {{publisher}}",
|
"publisher": "Dipublikasikan oleh {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Salin tautan",
|
"copy_link_to_clipboard": "Salin tautan",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "Memainkan sekarang",
|
"playing_now": "Memainkan sekarang",
|
||||||
"change": "Ubah",
|
"change": "Ubah",
|
||||||
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
|
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
|
||||||
"downloads_path": "Lokasi Unduhan",
|
|
||||||
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
|
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
|
||||||
"download_now": "Unduh sekarang",
|
"download_now": "Unduh sekarang",
|
||||||
"installation_instructions": "Instruksi Instalasi",
|
"installation_instructions": "Instruksi Instalasi",
|
||||||
|
@ -114,13 +109,11 @@
|
||||||
"verifying": "Memeriksa…",
|
"verifying": "Memeriksa…",
|
||||||
"completed_at": "Selesai pada {{date}}",
|
"completed_at": "Selesai pada {{date}}",
|
||||||
"completed": "Selesai",
|
"completed": "Selesai",
|
||||||
"cancelled": "Dibatalkan",
|
|
||||||
"download_again": "Unduh lagi",
|
"download_again": "Unduh lagi",
|
||||||
"cancel": "Batalkan",
|
"cancel": "Batalkan",
|
||||||
"filter": "Saring game yang diunduh",
|
"filter": "Saring game yang diunduh",
|
||||||
"remove": "Hapus",
|
"remove": "Hapus",
|
||||||
"downloading_metadata": "Mengunduh metadata…",
|
"downloading_metadata": "Mengunduh metadata…",
|
||||||
"checking_files": "Memeriksa file…",
|
|
||||||
"starting_download": "Memulai unduhan…",
|
"starting_download": "Memulai unduhan…",
|
||||||
"deleting": "Menghapus file instalasi…",
|
"deleting": "Menghapus file instalasi…",
|
||||||
"delete": "Hapus file instalasi",
|
"delete": "Hapus file instalasi",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Impostazioni",
|
"settings": "Impostazioni",
|
||||||
"my_library": "La mia libreria",
|
"my_library": "La mia libreria",
|
||||||
"downloading_metadata": "{{title}} (Scaricamento metadati…)",
|
"downloading_metadata": "{{title}} (Scaricamento metadati…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Verifica file…)",
|
|
||||||
"paused": "{{title}} (In pausa)",
|
"paused": "{{title}} (In pausa)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||||
"filter": "Filtra libreria",
|
"filter": "Filtra libreria",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Nessun download in corso",
|
"no_downloads_in_progress": "Nessun download in corso",
|
||||||
"downloading_metadata": "Scaricamento metadati di {{title}}…",
|
"downloading_metadata": "Scaricamento metadati di {{title}}…",
|
||||||
"checking_files": "Verifica file di {{title}}… ({{percentage}} completato)",
|
|
||||||
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
|
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} rimasto sul disco",
|
"space_left_on_disk": "{{space}} rimasto sul disco",
|
||||||
"eta": "Conclusione {{eta}}",
|
"eta": "Conclusione {{eta}}",
|
||||||
"downloading_metadata": "Scaricamento metadati…",
|
"downloading_metadata": "Scaricamento metadati…",
|
||||||
"checking_files": "Verifica file…",
|
|
||||||
"filter": "Filtra repack",
|
"filter": "Filtra repack",
|
||||||
"requirements": "Requisiti di sistema",
|
"requirements": "Requisiti di sistema",
|
||||||
"minimum": "Minimi",
|
"minimum": "Minimi",
|
||||||
"recommended": "Consigliati",
|
"recommended": "Consigliati",
|
||||||
"no_minimum_requirements": "{{title}} non fornisce informazioni sui requisiti minimi",
|
"no_minimum_requirements": "{{title}} non fornisce informazioni sui requisiti minimi",
|
||||||
"no_recommended_requirements": "{{title}} non fornisce informazioni sui requisiti consigliati",
|
"no_recommended_requirements": "{{title}} non fornisce informazioni sui requisiti consigliati",
|
||||||
"paused_progress": "{{progress}} (In pausa)",
|
|
||||||
"release_date": "Rilasciato il {{date}}",
|
"release_date": "Rilasciato il {{date}}",
|
||||||
"publisher": "Pubblicato da {{publisher}}",
|
"publisher": "Pubblicato da {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Copia link",
|
"copy_link_to_clipboard": "Copia link",
|
||||||
|
@ -120,22 +116,18 @@
|
||||||
"verifying": "Verifica…",
|
"verifying": "Verifica…",
|
||||||
"completed_at": "Completato in {{date}}",
|
"completed_at": "Completato in {{date}}",
|
||||||
"completed": "Completato",
|
"completed": "Completato",
|
||||||
"cancelled": "Annullato",
|
|
||||||
"download_again": "Scarica di nuovo",
|
"download_again": "Scarica di nuovo",
|
||||||
"cancel": "Annulla",
|
"cancel": "Annulla",
|
||||||
"filter": "Filtra giochi scaricati",
|
"filter": "Filtra giochi scaricati",
|
||||||
"remove": "Rimuovi",
|
"remove": "Rimuovi",
|
||||||
"downloading_metadata": "Scaricamento metadati…",
|
"downloading_metadata": "Scaricamento metadati…",
|
||||||
"checking_files": "Verifica file…",
|
|
||||||
"starting_download": "Avvio download…",
|
"starting_download": "Avvio download…",
|
||||||
"deleting": "Eliminazione dell'installer…",
|
"deleting": "Eliminazione dell'installer…",
|
||||||
"delete": "Rimuovi installer",
|
"delete": "Rimuovi installer",
|
||||||
"remove_from_list": "Rimuovi",
|
"remove_from_list": "Rimuovi",
|
||||||
"delete_modal_title": "Sei sicuro?",
|
"delete_modal_title": "Sei sicuro?",
|
||||||
"delete_modal_description": "Questo rimuoverà tutti i file di installazione dal tuo computer",
|
"delete_modal_description": "Questo rimuoverà tutti i file di installazione dal tuo computer",
|
||||||
"install": "Installa",
|
"install": "Installa"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Percorso dei download",
|
"downloads_path": "Percorso dei download",
|
||||||
|
@ -151,8 +143,7 @@
|
||||||
"general": "Generale",
|
"general": "Generale",
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"enable_real_debrid": "Abilita Real Debrid",
|
"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"
|
"save_changes": "Salva modifiche"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "설정",
|
"settings": "설정",
|
||||||
"my_library": "내 라이브러리",
|
"my_library": "내 라이브러리",
|
||||||
"downloading_metadata": "{{title}} (메타데이터 다운로드 중…)",
|
"downloading_metadata": "{{title}} (메타데이터 다운로드 중…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - 파일 검사 중…)",
|
|
||||||
"paused": "{{title}} (일시 정지됨)",
|
"paused": "{{title}} (일시 정지됨)",
|
||||||
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
||||||
"filter": "라이브러리 정렬",
|
"filter": "라이브러리 정렬",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "진행중인 다운로드 없음",
|
"no_downloads_in_progress": "진행중인 다운로드 없음",
|
||||||
"downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…",
|
"downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…",
|
||||||
"checking_files": "{{title}}의 파일들을 검사 중… ({{percentage}} 완료)",
|
|
||||||
"downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}"
|
"downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "여유 저장 용량 {{space}} 남음",
|
"space_left_on_disk": "여유 저장 용량 {{space}} 남음",
|
||||||
"eta": "완료까지 {{eta}}",
|
"eta": "완료까지 {{eta}}",
|
||||||
"downloading_metadata": "메타데이터 다운로드 중…",
|
"downloading_metadata": "메타데이터 다운로드 중…",
|
||||||
"checking_files": "파일 검사 중…",
|
|
||||||
"filter": "리팩들을 다음과 같이 정렬하기",
|
"filter": "리팩들을 다음과 같이 정렬하기",
|
||||||
"requirements": "시스템 사양",
|
"requirements": "시스템 사양",
|
||||||
"minimum": "최저 사양",
|
"minimum": "최저 사양",
|
||||||
"recommended": "권장 사양",
|
"recommended": "권장 사양",
|
||||||
"no_minimum_requirements": "{{title}}의 최저 사양을 제공받지 못 함",
|
"no_minimum_requirements": "{{title}}의 최저 사양을 제공받지 못 함",
|
||||||
"no_recommended_requirements": "{{title}}의 권장 사양을 제공받지 못 함",
|
"no_recommended_requirements": "{{title}}의 권장 사양을 제공받지 못 함",
|
||||||
"paused_progress": "{{progress}} (일시 정지)",
|
|
||||||
"release_date": "{{date}}에 발매됨",
|
"release_date": "{{date}}에 발매됨",
|
||||||
"publisher": "{{publisher}} 배급",
|
"publisher": "{{publisher}} 배급",
|
||||||
"copy_link_to_clipboard": "링크 복사하기",
|
"copy_link_to_clipboard": "링크 복사하기",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "현재 플레이 중",
|
"playing_now": "현재 플레이 중",
|
||||||
"change": "바꾸기",
|
"change": "바꾸기",
|
||||||
"repacks_modal_description": "다운로드 할 리팩을 선택해 주세요",
|
"repacks_modal_description": "다운로드 할 리팩을 선택해 주세요",
|
||||||
"downloads_path": "다운로드 경로",
|
|
||||||
"select_folder_hint": "기본 폴더를 바꾸려면 <0>설정</0>으로 가세요",
|
"select_folder_hint": "기본 폴더를 바꾸려면 <0>설정</0>으로 가세요",
|
||||||
"download_now": "지금 다운로드",
|
"download_now": "지금 다운로드",
|
||||||
"installation_instructions": "설치 방법",
|
"installation_instructions": "설치 방법",
|
||||||
|
@ -114,22 +109,18 @@
|
||||||
"verifying": "검증중…",
|
"verifying": "검증중…",
|
||||||
"completed_at": "{{date}}에 완료됨",
|
"completed_at": "{{date}}에 완료됨",
|
||||||
"completed": "완료됨",
|
"completed": "완료됨",
|
||||||
"cancelled": "취소됨",
|
|
||||||
"download_again": "다시 다운로드 하기",
|
"download_again": "다시 다운로드 하기",
|
||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"filter": "다운로드 된 게임들을 정렬하기",
|
"filter": "다운로드 된 게임들을 정렬하기",
|
||||||
"remove": "제거하기",
|
"remove": "제거하기",
|
||||||
"downloading_metadata": "메타데이터 다운로드 중…",
|
"downloading_metadata": "메타데이터 다운로드 중…",
|
||||||
"checking_files": "파일 검사 중…",
|
|
||||||
"starting_download": "다운로드 개시 중…",
|
"starting_download": "다운로드 개시 중…",
|
||||||
"deleting": "인스톨러 삭제 중…",
|
"deleting": "인스톨러 삭제 중…",
|
||||||
"delete": "인스톨러 삭제하기",
|
"delete": "인스톨러 삭제하기",
|
||||||
"remove_from_list": "제거하기",
|
"remove_from_list": "제거하기",
|
||||||
"delete_modal_title": "정말로 하시겠습니까?",
|
"delete_modal_title": "정말로 하시겠습니까?",
|
||||||
"delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다",
|
"delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다",
|
||||||
"install": "설치",
|
"install": "설치"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "다운로드 경로",
|
"downloads_path": "다운로드 경로",
|
||||||
|
@ -139,13 +130,11 @@
|
||||||
"enable_repack_list_notifications": "새 리팩이 추가되었을 때",
|
"enable_repack_list_notifications": "새 리팩이 추가되었을 때",
|
||||||
"telemetry": "자동 데이터 수집",
|
"telemetry": "자동 데이터 수집",
|
||||||
"telemetry_description": "익명 사용 통계를 활성화",
|
"telemetry_description": "익명 사용 통계를 활성화",
|
||||||
"real_debrid_api_token_description": "Real Debrid API 토큰",
|
|
||||||
"quit_app_instead_hiding": "작업 표시줄 트레이로 최소화하는 대신 Hydra를 종료",
|
"quit_app_instead_hiding": "작업 표시줄 트레이로 최소화하는 대신 Hydra를 종료",
|
||||||
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
|
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
|
||||||
"general": "일반",
|
"general": "일반",
|
||||||
"behavior": "행동",
|
"behavior": "행동",
|
||||||
"enable_real_debrid": "Real Debrid 활성화",
|
"enable_real_debrid": "Real-Debrid 활성화",
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
"real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
||||||
"save_changes": "변경 사항 저장"
|
"save_changes": "변경 사항 저장"
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Instellingen",
|
"settings": "Instellingen",
|
||||||
"my_library": "Mijn Bibliotheek",
|
"my_library": "Mijn Bibliotheek",
|
||||||
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Folders checken…)",
|
|
||||||
"paused": "{{title}} (Gepauzeerd)",
|
"paused": "{{title}} (Gepauzeerd)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filter Bibliotheek",
|
"filter": "Filter Bibliotheek",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Geen Downloads bezig",
|
"no_downloads_in_progress": "Geen Downloads bezig",
|
||||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
"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}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} Over op schijf",
|
"space_left_on_disk": "{{space}} Over op schijf",
|
||||||
"eta": "Conclusie {{eta}}",
|
"eta": "Conclusie {{eta}}",
|
||||||
"downloading_metadata": "Downloading metadata…",
|
"downloading_metadata": "Downloading metadata…",
|
||||||
"checking_files": "Files nakijken…",
|
|
||||||
"filter": "Filter repacks",
|
"filter": "Filter repacks",
|
||||||
"requirements": "Systeem vereisten",
|
"requirements": "Systeem vereisten",
|
||||||
"minimum": "Minimaal",
|
"minimum": "Minimaal",
|
||||||
"recommended": "Aanbevolen",
|
"recommended": "Aanbevolen",
|
||||||
"no_minimum_requirements": "{{title}} biedt geen informatie over de minimale vereisten",
|
"no_minimum_requirements": "{{title}} biedt geen informatie over de minimale vereisten",
|
||||||
"no_recommended_requirements": "{{title}} biedt geen informatie over aanbevolen vereisten",
|
"no_recommended_requirements": "{{title}} biedt geen informatie over aanbevolen vereisten",
|
||||||
"paused_progress": "{{progress}} (Paused)",
|
|
||||||
"release_date": "Uitgebracht op {{date}}",
|
"release_date": "Uitgebracht op {{date}}",
|
||||||
"publisher": "Gepubliceerd door {{publisher}}",
|
"publisher": "Gepubliceerd door {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Kopieer link",
|
"copy_link_to_clipboard": "Kopieer link",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "Speel nu",
|
"playing_now": "Speel nu",
|
||||||
"change": "Verander",
|
"change": "Verander",
|
||||||
"repacks_modal_description": "Kies de herverpakking die u wilt downloaden",
|
"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>",
|
"select_folder_hint": "Om de standaardmap te wijzigen, gaat u naar <0>instellingen</0>",
|
||||||
"download_now": "Download nu",
|
"download_now": "Download nu",
|
||||||
"installation_instructions": "Installatie instructies",
|
"installation_instructions": "Installatie instructies",
|
||||||
|
@ -114,22 +109,18 @@
|
||||||
"verifying": "Verifiëren…",
|
"verifying": "Verifiëren…",
|
||||||
"completed_at": "Voltooid binnen {{date}}",
|
"completed_at": "Voltooid binnen {{date}}",
|
||||||
"completed": "Voltooid",
|
"completed": "Voltooid",
|
||||||
"cancelled": "Geannuleerd",
|
|
||||||
"download_again": "Opnieuw downloaden",
|
"download_again": "Opnieuw downloaden",
|
||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
"filter": "Filter gedownloade games",
|
"filter": "Filter gedownloade games",
|
||||||
"remove": "Verwijderen",
|
"remove": "Verwijderen",
|
||||||
"downloading_metadata": "Metagegevens downloaden",
|
"downloading_metadata": "Metagegevens downloaden",
|
||||||
"checking_files": "Bestanden controleren",
|
|
||||||
"starting_download": "download starten",
|
"starting_download": "download starten",
|
||||||
"deleting": "Installatieprogramma verwijderen…",
|
"deleting": "Installatieprogramma verwijderen…",
|
||||||
"delete": "Installatieprogramma verwijderen",
|
"delete": "Installatieprogramma verwijderen",
|
||||||
"remove_from_list": "Verwijderen",
|
"remove_from_list": "Verwijderen",
|
||||||
"delete_modal_title": "Weet je het zeker?",
|
"delete_modal_title": "Weet je het zeker?",
|
||||||
"delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd",
|
"delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd",
|
||||||
"install": "Installeren",
|
"install": "Installeren"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloadpad",
|
"downloads_path": "Downloadpad",
|
||||||
|
@ -139,13 +130,12 @@
|
||||||
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
|
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
|
||||||
"telemetry": "Telemetrie",
|
"telemetry": "Telemetrie",
|
||||||
"telemetry_description": "Schakel anonieme gebruiksstatistieken in",
|
"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",
|
"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",
|
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
|
||||||
"general": "Algemeen",
|
"general": "Algemeen",
|
||||||
"behavior": "Gedrag",
|
"behavior": "Gedrag",
|
||||||
"enable_real_debrid": "Enable Real Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
||||||
"save_changes": "Wijzigingen opslaan"
|
"save_changes": "Wijzigingen opslaan"
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Ustawienia",
|
"settings": "Ustawienia",
|
||||||
"my_library": "Moja biblioteka",
|
"my_library": "Moja biblioteka",
|
||||||
"downloading_metadata": "{{title}} (Pobieranie metadata…)",
|
"downloading_metadata": "{{title}} (Pobieranie metadata…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Sprawdzanie plików…)",
|
|
||||||
"paused": "{{title}} (Zatrzymano)",
|
"paused": "{{title}} (Zatrzymano)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||||
"filter": "Filtruj biblioteke",
|
"filter": "Filtruj biblioteke",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Brak pobierań w toku",
|
"no_downloads_in_progress": "Brak pobierań w toku",
|
||||||
"downloading_metadata": "Pobieranie {{title}} metadata…",
|
"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}}"
|
"downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} wolnego na dysku",
|
"space_left_on_disk": "{{space}} wolnego na dysku",
|
||||||
"eta": "Podsumowanie {{eta}}",
|
"eta": "Podsumowanie {{eta}}",
|
||||||
"downloading_metadata": "Pobieranie metadata…",
|
"downloading_metadata": "Pobieranie metadata…",
|
||||||
"checking_files": "Sprawdzanie plików…",
|
|
||||||
"filter": "Filtruj repacki",
|
"filter": "Filtruj repacki",
|
||||||
"requirements": "Wymagania systemowe",
|
"requirements": "Wymagania systemowe",
|
||||||
"minimum": "Minimalne",
|
"minimum": "Minimalne",
|
||||||
"recommended": "Zalecane",
|
"recommended": "Zalecane",
|
||||||
"no_minimum_requirements": "{{title}} nie zawiera informacji o minimalnych wymaganiach",
|
"no_minimum_requirements": "{{title}} nie zawiera informacji o minimalnych wymaganiach",
|
||||||
"no_recommended_requirements": "{{title}} nie zawiera informacji o zalecanych wymaganiach",
|
"no_recommended_requirements": "{{title}} nie zawiera informacji o zalecanych wymaganiach",
|
||||||
"paused_progress": "{{progress}} (Zatrzymano)",
|
|
||||||
"release_date": "Wydano w {{date}}",
|
"release_date": "Wydano w {{date}}",
|
||||||
"publisher": "Opublikowany przez {{publisher}}",
|
"publisher": "Opublikowany przez {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Kopiuj łącze",
|
"copy_link_to_clipboard": "Kopiuj łącze",
|
||||||
|
@ -120,22 +116,18 @@
|
||||||
"verifying": "Weryfikowanie…",
|
"verifying": "Weryfikowanie…",
|
||||||
"completed_at": "Zakończono w {{date}}",
|
"completed_at": "Zakończono w {{date}}",
|
||||||
"completed": "Zakończono",
|
"completed": "Zakończono",
|
||||||
"cancelled": "Anulowano",
|
|
||||||
"download_again": "Pobierz ponownie",
|
"download_again": "Pobierz ponownie",
|
||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"filter": "Filtruj pobrane gry",
|
"filter": "Filtruj pobrane gry",
|
||||||
"remove": "Usuń",
|
"remove": "Usuń",
|
||||||
"downloading_metadata": "Pobieranie metadata…",
|
"downloading_metadata": "Pobieranie metadata…",
|
||||||
"checking_files": "Sprawdzanie plików…",
|
|
||||||
"starting_download": "Rozpoczęto pobieranie…",
|
"starting_download": "Rozpoczęto pobieranie…",
|
||||||
"deleting": "Usuwanie instalatora…",
|
"deleting": "Usuwanie instalatora…",
|
||||||
"delete": "Usuń instalator",
|
"delete": "Usuń instalator",
|
||||||
"remove_from_list": "Usuń",
|
"remove_from_list": "Usuń",
|
||||||
"delete_modal_title": "Czy na pewno?",
|
"delete_modal_title": "Czy na pewno?",
|
||||||
"delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera",
|
"delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera",
|
||||||
"install": "Instaluj",
|
"install": "Instaluj"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Ścieżka pobierania",
|
"downloads_path": "Ścieżka pobierania",
|
||||||
|
@ -145,14 +137,13 @@
|
||||||
"enable_repack_list_notifications": "Gdy dodawany jest nowy repack",
|
"enable_repack_list_notifications": "Gdy dodawany jest nowy repack",
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetria",
|
||||||
"telemetry_description": "Włącz anonimowe statystyki użycia",
|
"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",
|
"quit_app_instead_hiding": "Zamknij Hydr zamiast minimalizować do zasobnika",
|
||||||
"launch_with_system": "Uruchom Hydra przy starcie systemu",
|
"launch_with_system": "Uruchom Hydra przy starcie systemu",
|
||||||
"general": "Ogólne",
|
"general": "Ogólne",
|
||||||
"behavior": "Zachowania",
|
"behavior": "Zachowania",
|
||||||
"enable_real_debrid": "Włącz Real Debrid",
|
"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>",
|
||||||
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>.",
|
|
||||||
"save_changes": "Zapisz zmiany"
|
"save_changes": "Zapisz zmiany"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -9,10 +9,9 @@
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"settings": "Configurações",
|
"settings": "Ajustes",
|
||||||
"my_library": "Minha biblioteca",
|
"my_library": "Minha biblioteca",
|
||||||
"downloading_metadata": "{{title}} (Baixando metadados…)",
|
"downloading_metadata": "{{title}} (Baixando metadados…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Verificando arquivos…)",
|
|
||||||
"paused": "{{title}} (Pausado)",
|
"paused": "{{title}} (Pausado)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Baixando…)",
|
"downloading": "{{title}} ({{percentage}} - Baixando…)",
|
||||||
"filter": "Filtrar biblioteca",
|
"filter": "Filtrar biblioteca",
|
||||||
|
@ -28,15 +27,15 @@
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"search_results": "Resultados da busca",
|
"search_results": "Resultados da busca",
|
||||||
"settings": "Configurações",
|
"settings": "Ajustes",
|
||||||
"home": "Início",
|
"home": "Início",
|
||||||
"version_available": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar."
|
"version_available": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Sem downloads em andamento",
|
"no_downloads_in_progress": "Sem downloads em andamento",
|
||||||
"downloading_metadata": "Baixando metadados de {{title}}…",
|
"downloading_metadata": "Baixando metadados de {{title}}…",
|
||||||
"checking_files": "Verificando arquivos de {{title}}… ({{percentage}} completo)",
|
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
|
||||||
"downloading": "Baixando {{title}}… ({{percentage}} completo) - Conclusão {{eta}} - {{speed}}"
|
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Ver opções de download",
|
"open_download_options": "Ver opções de download",
|
||||||
|
@ -51,15 +50,15 @@
|
||||||
"remove_from_list": "Remover",
|
"remove_from_list": "Remover",
|
||||||
"space_left_on_disk": "{{space}} livres em disco",
|
"space_left_on_disk": "{{space}} livres em disco",
|
||||||
"eta": "Conclusão {{eta}}",
|
"eta": "Conclusão {{eta}}",
|
||||||
|
"calculating_eta": "Calculando tempo restante…",
|
||||||
"downloading_metadata": "Baixando metadados…",
|
"downloading_metadata": "Baixando metadados…",
|
||||||
"checking_files": "Verificando arquivos…",
|
|
||||||
"filter": "Filtrar repacks",
|
"filter": "Filtrar repacks",
|
||||||
"requirements": "Requisitos do sistema",
|
"requirements": "Requisitos do sistema",
|
||||||
"minimum": "Mínimos",
|
"minimum": "Mínimos",
|
||||||
"recommended": "Recomendados",
|
"recommended": "Recomendados",
|
||||||
"no_minimum_requirements": "{{title}} não possui informações de requisitos mínimos",
|
"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",
|
"no_recommended_requirements": "{{title}} não possui informações de requisitos recomendados",
|
||||||
"paused_progress": "{{progress}} (Pausado)",
|
"paused": "Pausado",
|
||||||
"release_date": "Lançado em {{date}}",
|
"release_date": "Lançado em {{date}}",
|
||||||
"publisher": "Publicado por {{publisher}}",
|
"publisher": "Publicado por {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Copiar link",
|
"copy_link_to_clipboard": "Copiar link",
|
||||||
|
@ -83,8 +82,8 @@
|
||||||
"playing_now": "Jogando agora",
|
"playing_now": "Jogando agora",
|
||||||
"change": "Mudar",
|
"change": "Mudar",
|
||||||
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
|
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
|
||||||
"select_folder_hint": "Para trocar a pasta padrão, acesse a <0>Tela de Configurações</0>",
|
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes</0>",
|
||||||
"download_now": "Baixe agora",
|
"download_now": "Iniciar download",
|
||||||
"installation_instructions": "Instruções de Instalação",
|
"installation_instructions": "Instruções de Instalação",
|
||||||
"installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo",
|
"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:",
|
"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",
|
"previous_screenshot": "Captura de tela anterior",
|
||||||
"next_screenshot": "Próxima captura de tela",
|
"next_screenshot": "Próxima captura de tela",
|
||||||
"screenshot": "Captura de tela {{number}}",
|
"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": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -117,22 +118,19 @@
|
||||||
"verifying": "Verificando…",
|
"verifying": "Verificando…",
|
||||||
"completed_at": "Concluído em {{date}}",
|
"completed_at": "Concluído em {{date}}",
|
||||||
"completed": "Concluído",
|
"completed": "Concluído",
|
||||||
"cancelled": "Cancelado",
|
"removed": "Não baixado",
|
||||||
"download_again": "Baixar novamente",
|
"download_again": "Baixar novamente",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"filter": "Filtrar jogos baixados",
|
"filter": "Filtrar jogos baixados",
|
||||||
"remove": "Remover",
|
"remove": "Remover",
|
||||||
"downloading_metadata": "Baixando metadados…",
|
"downloading_metadata": "Baixando metadados…",
|
||||||
"checking_files": "Verificando arquivos…",
|
|
||||||
"starting_download": "Iniciando download…",
|
"starting_download": "Iniciando download…",
|
||||||
"remove_from_list": "Remover",
|
"remove_from_list": "Remover",
|
||||||
"delete": "Remover instalador",
|
"delete": "Remover instalador",
|
||||||
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
||||||
"delete_modal_title": "Tem certeza?",
|
"delete_modal_title": "Tem certeza?",
|
||||||
"deleting": "Excluindo instalador…",
|
"deleting": "Excluindo instalador…",
|
||||||
"install": "Instalar",
|
"install": "Instalar"
|
||||||
"torrent": "Torrent",
|
|
||||||
"real_debrid": "Real Debrid"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Diretório dos downloads",
|
"downloads_path": "Diretório dos downloads",
|
||||||
|
@ -142,15 +140,20 @@
|
||||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetria",
|
||||||
"telemetry_description": "Habilitar estatísticas de uso anônimas",
|
"telemetry_description": "Habilitar estatísticas de uso anônimas",
|
||||||
"real_debrid_api_token_label": "Token de API do Real Debrid",
|
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
||||||
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo",
|
"quit_app_instead_hiding": "Encerrar o Hydra ao invés de minimizá-lo ao fechar",
|
||||||
"launch_with_system": "Iniciar aplicativo na inicialização do sistema",
|
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
||||||
"general": "Geral",
|
"general": "Geral",
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
"enable_real_debrid": "Habilitar Real Debrid",
|
"real_debrid_api_token": "Token de API",
|
||||||
"real_debrid": "Real Debrid",
|
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui</0>.",
|
"real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
||||||
"save_changes": "Salvar mudanças"
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
@ -177,11 +180,5 @@
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Botão de fechar"
|
"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": "Настройки",
|
"settings": "Настройки",
|
||||||
"my_library": "Библиотека",
|
"my_library": "Библиотека",
|
||||||
"downloading_metadata": "{{title}} (Загрузка метаданных…)",
|
"downloading_metadata": "{{title}} (Загрузка метаданных…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Проверка файлов…)",
|
|
||||||
"paused": "{{title}} (Приостановлено)",
|
"paused": "{{title}} (Приостановлено)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Загрузка…)",
|
"downloading": "{{title}} ({{percentage}} - Загрузка…)",
|
||||||
"filter": "Фильтр библиотеки",
|
"filter": "Фильтр библиотеки",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Нет активных загрузок",
|
"no_downloads_in_progress": "Нет активных загрузок",
|
||||||
"downloading_metadata": "Загрузка метаданных {{title}}…",
|
"downloading_metadata": "Загрузка метаданных {{title}}…",
|
||||||
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
|
|
||||||
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}"
|
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} свободно на диске",
|
"space_left_on_disk": "{{space}} свободно на диске",
|
||||||
"eta": "Окончание {{eta}}",
|
"eta": "Окончание {{eta}}",
|
||||||
"downloading_metadata": "Загрузка метаданных…",
|
"downloading_metadata": "Загрузка метаданных…",
|
||||||
"checking_files": "Проверка файлов…",
|
|
||||||
"filter": "Фильтр репаков",
|
"filter": "Фильтр репаков",
|
||||||
"requirements": "Системные требования",
|
"requirements": "Системные требования",
|
||||||
"minimum": "Минимальные",
|
"minimum": "Минимальные",
|
||||||
"recommended": "Рекомендуемые",
|
"recommended": "Рекомендуемые",
|
||||||
"no_minimum_requirements": "Для {{title}} не указаны минимальные требования",
|
"no_minimum_requirements": "Для {{title}} не указаны минимальные требования",
|
||||||
"no_recommended_requirements": "Для {{title}} не указаны рекомендуемые требования",
|
"no_recommended_requirements": "Для {{title}} не указаны рекомендуемые требования",
|
||||||
"paused_progress": "{{progress}} (Приостановлено)",
|
|
||||||
"release_date": "Выпущено {{date}}",
|
"release_date": "Выпущено {{date}}",
|
||||||
"publisher": "Издатель {{publisher}}",
|
"publisher": "Издатель {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Копировать ссылку",
|
"copy_link_to_clipboard": "Копировать ссылку",
|
||||||
|
@ -120,22 +116,18 @@
|
||||||
"verifying": "Проверка…",
|
"verifying": "Проверка…",
|
||||||
"completed_at": "Завершено в {{date}}",
|
"completed_at": "Завершено в {{date}}",
|
||||||
"completed": "Завершено",
|
"completed": "Завершено",
|
||||||
"cancelled": "Отменено",
|
|
||||||
"download_again": "Загрузить снова",
|
"download_again": "Загрузить снова",
|
||||||
"cancel": "Отменить",
|
"cancel": "Отменить",
|
||||||
"filter": "Фильтр загруженных игр",
|
"filter": "Фильтр загруженных игр",
|
||||||
"remove": "Удалить",
|
"remove": "Удалить",
|
||||||
"downloading_metadata": "Загрузка метаданных…",
|
"downloading_metadata": "Загрузка метаданных…",
|
||||||
"checking_files": "Проверка файлов…",
|
|
||||||
"starting_download": "Начало загрузки…",
|
"starting_download": "Начало загрузки…",
|
||||||
"deleting": "Удаление установщика…",
|
"deleting": "Удаление установщика…",
|
||||||
"delete": "Удалить установщик",
|
"delete": "Удалить установщик",
|
||||||
"remove_from_list": "Удалить",
|
"remove_from_list": "Удалить",
|
||||||
"delete_modal_title": "Вы уверены?",
|
"delete_modal_title": "Вы уверены?",
|
||||||
"delete_modal_description": "Это удалит все установщики с вашего компьютера",
|
"delete_modal_description": "Это удалит все установщики с вашего компьютера",
|
||||||
"install": "Установить",
|
"install": "Установить"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "Torrent"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Путь загрузок",
|
"downloads_path": "Путь загрузок",
|
||||||
|
@ -145,14 +137,13 @@
|
||||||
"enable_repack_list_notifications": "При добавлении нового репака",
|
"enable_repack_list_notifications": "При добавлении нового репака",
|
||||||
"telemetry": "Телеметрия",
|
"telemetry": "Телеметрия",
|
||||||
"telemetry_description": "Отправлять анонимную статистику использования",
|
"telemetry_description": "Отправлять анонимную статистику использования",
|
||||||
"real_debrid_api_token_label": "Real Debrid API-токен",
|
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||||
"quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей",
|
"quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей",
|
||||||
"launch_with_system": "Запуск Hydra вместе с системой",
|
"launch_with_system": "Запуск Hydra вместе с системой",
|
||||||
"general": "Основные",
|
"general": "Основные",
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"enable_real_debrid": "Включить Real Debrid",
|
"enable_real_debrid": "Включить Real-Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>",
|
||||||
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>.",
|
|
||||||
"save_changes": "Сохранить изменения"
|
"save_changes": "Сохранить изменения"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Ayarlar",
|
"settings": "Ayarlar",
|
||||||
"my_library": "Kütüphane",
|
"my_library": "Kütüphane",
|
||||||
"downloading_metadata": "{{title}} (Metadata indiriliyor…)",
|
"downloading_metadata": "{{title}} (Metadata indiriliyor…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Dosyalar kontrol ediliyor…)",
|
|
||||||
"paused": "{{title}} (Duraklatıldı)",
|
"paused": "{{title}} (Duraklatıldı)",
|
||||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||||
"filter": "Kütüphaneyi filtrele",
|
"filter": "Kütüphaneyi filtrele",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "İndirilen bir şey yok",
|
"no_downloads_in_progress": "İndirilen bir şey yok",
|
||||||
"downloading_metadata": "{{title}} metadatası indiriliyor…",
|
"downloading_metadata": "{{title}} metadatası indiriliyor…",
|
||||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
|
|
||||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
|
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "Diskte {{space}} yer kaldı",
|
"space_left_on_disk": "Diskte {{space}} yer kaldı",
|
||||||
"eta": "Bitiş {{eta}}",
|
"eta": "Bitiş {{eta}}",
|
||||||
"downloading_metadata": "Metadata indiriliyor…",
|
"downloading_metadata": "Metadata indiriliyor…",
|
||||||
"checking_files": "Dosyalar kontrol ediliyor…",
|
|
||||||
"filter": "Repackleri filtrele",
|
"filter": "Repackleri filtrele",
|
||||||
"requirements": "Sistem gereksinimleri",
|
"requirements": "Sistem gereksinimleri",
|
||||||
"minimum": "Minimum",
|
"minimum": "Minimum",
|
||||||
"recommended": "Önerilen",
|
"recommended": "Önerilen",
|
||||||
"no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor",
|
"no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor",
|
||||||
"no_recommended_requirements": "{{title}} önerilen 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ı",
|
"release_date": "{{date}} tarihinde çıktı",
|
||||||
"publisher": "{{publisher}} tarihinde yayınlandı",
|
"publisher": "{{publisher}} tarihinde yayınlandı",
|
||||||
"copy_link_to_clipboard": "Link'i kopyala",
|
"copy_link_to_clipboard": "Link'i kopyala",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "Şimdi oynanıyor",
|
"playing_now": "Şimdi oynanıyor",
|
||||||
"change": "Değiştir",
|
"change": "Değiştir",
|
||||||
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
|
"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",
|
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
|
||||||
"download_now": "Şimdi",
|
"download_now": "Şimdi",
|
||||||
"installation_instructions": "Kurulum",
|
"installation_instructions": "Kurulum",
|
||||||
|
@ -114,13 +109,11 @@
|
||||||
"verifying": "Doğrulanıyor…",
|
"verifying": "Doğrulanıyor…",
|
||||||
"completed_at": "{{date}} tarihinde tamamlanacak",
|
"completed_at": "{{date}} tarihinde tamamlanacak",
|
||||||
"completed": "Tamamlandı",
|
"completed": "Tamamlandı",
|
||||||
"cancelled": "İptal edildi",
|
|
||||||
"download_again": "Tekrar indir",
|
"download_again": "Tekrar indir",
|
||||||
"cancel": "İptal et",
|
"cancel": "İptal et",
|
||||||
"filter": "Yüklü oyunları filtrele",
|
"filter": "Yüklü oyunları filtrele",
|
||||||
"remove": "Kaldır",
|
"remove": "Kaldır",
|
||||||
"downloading_metadata": "Metadata indiriliyor…",
|
"downloading_metadata": "Metadata indiriliyor…",
|
||||||
"checking_files": "Dosyalar kontrol ediliyor…",
|
|
||||||
"starting_download": "İndirme başlatılıyor…",
|
"starting_download": "İndirme başlatılıyor…",
|
||||||
"deleting": "Installer siliniyor…",
|
"deleting": "Installer siliniyor…",
|
||||||
"delete": "Installer'ı sil",
|
"delete": "Installer'ı sil",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "Налаштування",
|
"settings": "Налаштування",
|
||||||
"my_library": "Бібліотека",
|
"my_library": "Бібліотека",
|
||||||
"downloading_metadata": "{{title}} (Завантаження метаданих…)",
|
"downloading_metadata": "{{title}} (Завантаження метаданих…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - Перевірка файлів…)",
|
|
||||||
"paused": "{{title}} (Призупинено)",
|
"paused": "{{title}} (Призупинено)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Завантаження…)",
|
"downloading": "{{title}} ({{percentage}} - Завантаження…)",
|
||||||
"filter": "Фільтр бібліотеки",
|
"filter": "Фільтр бібліотеки",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Немає активних завантажень",
|
"no_downloads_in_progress": "Немає активних завантажень",
|
||||||
"downloading_metadata": "Завантаження метаданих {{title}}…",
|
"downloading_metadata": "Завантаження метаданих {{title}}…",
|
||||||
"checking_files": "Перевірка файлів {{title}}… ({{percentage}} завершено)",
|
|
||||||
"downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}"
|
"downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "{{space}} вільно на диску",
|
"space_left_on_disk": "{{space}} вільно на диску",
|
||||||
"eta": "Закінчення {{eta}}",
|
"eta": "Закінчення {{eta}}",
|
||||||
"downloading_metadata": "Завантаження метаданих…",
|
"downloading_metadata": "Завантаження метаданих…",
|
||||||
"checking_files": "Перевірка файлів…",
|
|
||||||
"filter": "Фільтр репаків",
|
"filter": "Фільтр репаків",
|
||||||
"requirements": "Системні вимоги",
|
"requirements": "Системні вимоги",
|
||||||
"minimum": "Мінімальні",
|
"minimum": "Мінімальні",
|
||||||
"recommended": "Рекомендовані",
|
"recommended": "Рекомендовані",
|
||||||
"no_minimum_requirements": "Для {{title}} не вказані мінімальні вимоги",
|
"no_minimum_requirements": "Для {{title}} не вказані мінімальні вимоги",
|
||||||
"no_recommended_requirements": "Для {{title}} не вказані рекомендовані вимоги",
|
"no_recommended_requirements": "Для {{title}} не вказані рекомендовані вимоги",
|
||||||
"paused_progress": "{{progress}} (Призупинено)",
|
|
||||||
"release_date": "Випущено {{date}}",
|
"release_date": "Випущено {{date}}",
|
||||||
"publisher": "Видавець {{publisher}}",
|
"publisher": "Видавець {{publisher}}",
|
||||||
"copy_link_to_clipboard": "Скопіювати посилання",
|
"copy_link_to_clipboard": "Скопіювати посилання",
|
||||||
|
@ -86,7 +82,6 @@
|
||||||
"playing_now": "Поточна гра",
|
"playing_now": "Поточна гра",
|
||||||
"change": "Змінити",
|
"change": "Змінити",
|
||||||
"repacks_modal_description": "Виберіть репак, який хочете завантажити",
|
"repacks_modal_description": "Виберіть репак, який хочете завантажити",
|
||||||
"downloads_path": "Шлях завантажень",
|
|
||||||
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
|
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
|
||||||
"download_now": "Завантажити зараз",
|
"download_now": "Завантажити зараз",
|
||||||
"installation_instructions": "Інструкція зі встановлення",
|
"installation_instructions": "Інструкція зі встановлення",
|
||||||
|
@ -114,13 +109,11 @@
|
||||||
"verifying": "Перевірка…",
|
"verifying": "Перевірка…",
|
||||||
"completed_at": "Завершено в {{date}}",
|
"completed_at": "Завершено в {{date}}",
|
||||||
"completed": "Завершено",
|
"completed": "Завершено",
|
||||||
"cancelled": "Скасовано",
|
|
||||||
"download_again": "Завантажити знову",
|
"download_again": "Завантажити знову",
|
||||||
"cancel": "Скасувати",
|
"cancel": "Скасувати",
|
||||||
"filter": "Фільтр завантажених ігор",
|
"filter": "Фільтр завантажених ігор",
|
||||||
"remove": "Видалити",
|
"remove": "Видалити",
|
||||||
"downloading_metadata": "Завантаження метаданих…",
|
"downloading_metadata": "Завантаження метаданих…",
|
||||||
"checking_files": "Перевірка файлів…",
|
|
||||||
"starting_download": "Початок завантаження…",
|
"starting_download": "Початок завантаження…",
|
||||||
"deleting": "Видалення інсталятора…",
|
"deleting": "Видалення інсталятора…",
|
||||||
"delete": "Видалити інсталятор",
|
"delete": "Видалити інсталятор",
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"my_library": "我的游戏库",
|
"my_library": "我的游戏库",
|
||||||
"downloading_metadata": "{{title}} (正在下载元数据…)",
|
"downloading_metadata": "{{title}} (正在下载元数据…)",
|
||||||
"checking_files": "{{title}} ({{percentage}} - 正在检查文件…)",
|
|
||||||
"paused": "{{title}} (已暂停)",
|
"paused": "{{title}} (已暂停)",
|
||||||
"downloading": "{{title}} ({{percentage}} - 正在下载…)",
|
"downloading": "{{title}} ({{percentage}} - 正在下载…)",
|
||||||
"filter": "筛选游戏库",
|
"filter": "筛选游戏库",
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "没有正在进行的下载",
|
"no_downloads_in_progress": "没有正在进行的下载",
|
||||||
"downloading_metadata": "正在下载{{title}}的元数据…",
|
"downloading_metadata": "正在下载{{title}}的元数据…",
|
||||||
"checking_files": "正在检查{{title}}的文件… ({{percentage}}完成)",
|
|
||||||
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}"
|
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
|
@ -56,14 +54,12 @@
|
||||||
"space_left_on_disk": "磁盘剩余空间{{space}}",
|
"space_left_on_disk": "磁盘剩余空间{{space}}",
|
||||||
"eta": "预计完成时间{{eta}}",
|
"eta": "预计完成时间{{eta}}",
|
||||||
"downloading_metadata": "正在下载元数据…",
|
"downloading_metadata": "正在下载元数据…",
|
||||||
"checking_files": "正在检查文件…",
|
|
||||||
"filter": "筛选重打包",
|
"filter": "筛选重打包",
|
||||||
"requirements": "配置要求",
|
"requirements": "配置要求",
|
||||||
"minimum": "最低要求",
|
"minimum": "最低要求",
|
||||||
"recommended": "推荐要求",
|
"recommended": "推荐要求",
|
||||||
"no_minimum_requirements": "{{title}}没有提供最低要求信息",
|
"no_minimum_requirements": "{{title}}没有提供最低要求信息",
|
||||||
"no_recommended_requirements": "{{title}}没有提供推荐要求信息",
|
"no_recommended_requirements": "{{title}}没有提供推荐要求信息",
|
||||||
"paused_progress": "{{progress}} (已暂停)",
|
|
||||||
"release_date": "发布于{{date}}",
|
"release_date": "发布于{{date}}",
|
||||||
"publisher": "发行商{{publisher}}",
|
"publisher": "发行商{{publisher}}",
|
||||||
"copy_link_to_clipboard": "复制链接",
|
"copy_link_to_clipboard": "复制链接",
|
||||||
|
@ -86,9 +82,7 @@
|
||||||
"playing_now": "正在游戏中",
|
"playing_now": "正在游戏中",
|
||||||
"change": "更改",
|
"change": "更改",
|
||||||
"repacks_modal_description": "选择您想要下载的重打包",
|
"repacks_modal_description": "选择您想要下载的重打包",
|
||||||
"downloads_path": "下载路径",
|
|
||||||
"select_folder_hint": "要更改默认文件夹,请访问",
|
"select_folder_hint": "要更改默认文件夹,请访问",
|
||||||
"settings": "设置",
|
|
||||||
"download_now": "立即下载",
|
"download_now": "立即下载",
|
||||||
"installation_instructions": "安装说明",
|
"installation_instructions": "安装说明",
|
||||||
"installation_instructions_description": "安装这个游戏需要额外的步骤",
|
"installation_instructions_description": "安装这个游戏需要额外的步骤",
|
||||||
|
@ -118,22 +112,18 @@
|
||||||
"verifying": "正在验证…",
|
"verifying": "正在验证…",
|
||||||
"completed_at": "完成于{{date}}",
|
"completed_at": "完成于{{date}}",
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"cancelled": "已取消",
|
|
||||||
"download_again": "再次下载",
|
"download_again": "再次下载",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"filter": "筛选已下载游戏",
|
"filter": "筛选已下载游戏",
|
||||||
"remove": "移除",
|
"remove": "移除",
|
||||||
"downloading_metadata": "正在下载元数据…",
|
"downloading_metadata": "正在下载元数据…",
|
||||||
"checking_files": "正在检查文件…",
|
|
||||||
"starting_download": "开始下载…",
|
"starting_download": "开始下载…",
|
||||||
"deleting": "正在删除安装程序…",
|
"deleting": "正在删除安装程序…",
|
||||||
"delete": "移除安装程序",
|
"delete": "移除安装程序",
|
||||||
"remove_from_list": "移除",
|
"remove_from_list": "移除",
|
||||||
"delete_modal_title": "您确定吗?",
|
"delete_modal_title": "您确定吗?",
|
||||||
"delete_modal_description": "这将从您的电脑上移除所有的安装文件",
|
"delete_modal_description": "这将从您的电脑上移除所有的安装文件",
|
||||||
"install": "安装",
|
"install": "安装"
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"torrent": "种子"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "下载路径",
|
"downloads_path": "下载路径",
|
||||||
|
@ -143,13 +133,11 @@
|
||||||
"enable_repack_list_notifications": "添加新重打包时",
|
"enable_repack_list_notifications": "添加新重打包时",
|
||||||
"telemetry": "遥测",
|
"telemetry": "遥测",
|
||||||
"telemetry_description": "启用匿名使用统计",
|
"telemetry_description": "启用匿名使用统计",
|
||||||
"real_debrid_api_token_description": "Real Debrid API密钥",
|
|
||||||
"behavior": "行为",
|
"behavior": "行为",
|
||||||
"general": "常规",
|
"general": "常规",
|
||||||
"quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘",
|
"quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘",
|
||||||
"launch_with_system": "随系统启动时运行应用程序",
|
"launch_with_system": "随系统启动时运行应用程序",
|
||||||
"enable_real_debrid": "启用 Real Debrid",
|
"enable_real_debrid": "启用 Real-Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
|
||||||
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
||||||
"save_changes": "保存更改"
|
"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 { Repack } from "./repack.entity";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { Downloader, GameStatus } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
|
import type { Aria2Status } from "aria2";
|
||||||
|
|
||||||
@Entity("game")
|
@Entity("game")
|
||||||
export class Game {
|
export class Game {
|
||||||
|
@ -42,7 +43,7 @@ export class Game {
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
status: GameStatus | null;
|
status: Aria2Status | null;
|
||||||
|
|
||||||
@Column("int", { default: Downloader.Torrent })
|
@Column("int", { default: Downloader.Torrent })
|
||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
|
@ -53,9 +54,6 @@ export class Game {
|
||||||
@Column("float", { default: 0 })
|
@Column("float", { default: 0 })
|
||||||
progress: number;
|
progress: number;
|
||||||
|
|
||||||
@Column("float", { default: 0 })
|
|
||||||
fileVerificationProgress: number;
|
|
||||||
|
|
||||||
@Column("int", { default: 0 })
|
@Column("int", { default: 0 })
|
||||||
bytesDownloaded: number;
|
bytesDownloaded: number;
|
||||||
|
|
||||||
|
|
|
@ -10,24 +10,23 @@ const sendEvent = (event: AppUpdaterEvents) => {
|
||||||
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockValuesForDebug = async () => {
|
const mockValuesForDebug = () => {
|
||||||
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
||||||
// sendEvent({ type: "update-downloaded" });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
autoUpdater
|
autoUpdater
|
||||||
.addListener("update-available", (info: UpdateInfo) => {
|
.once("update-available", (info: UpdateInfo) => {
|
||||||
sendEvent({ type: "update-available", info });
|
sendEvent({ type: "update-available", info });
|
||||||
})
|
})
|
||||||
.addListener("update-downloaded", () => {
|
.once("update-downloaded", () => {
|
||||||
sendEvent({ type: "update-downloaded" });
|
sendEvent({ type: "update-downloaded" });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
autoUpdater.checkForUpdates();
|
autoUpdater.checkForUpdates();
|
||||||
} else {
|
} else {
|
||||||
await mockValuesForDebug();
|
mockValuesForDebug();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import "./user-preferences/update-user-preferences";
|
||||||
import "./user-preferences/auto-launch";
|
import "./user-preferences/auto-launch";
|
||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
|
|
|
@ -10,7 +10,7 @@ const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectID: string,
|
objectID: string,
|
||||||
title: string,
|
title: string,
|
||||||
gameShop: GameShop,
|
shop: GameShop,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => {
|
) => {
|
||||||
return gameRepository
|
return gameRepository
|
||||||
|
@ -19,7 +19,7 @@ const addGameToLibrary = async (
|
||||||
objectID,
|
objectID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shop: gameShop,
|
shop,
|
||||||
status: null,
|
status: null,
|
||||||
executablePath,
|
executablePath,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
|
@ -40,7 +40,7 @@ const addGameToLibrary = async (
|
||||||
title,
|
title,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
objectID,
|
objectID,
|
||||||
shop: gameShop,
|
shop,
|
||||||
executablePath,
|
executablePath,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
import { GameStatus } from "@shared";
|
import { In } from "typeorm";
|
||||||
|
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
|
@ -15,7 +16,7 @@ const deleteGameFolder = async (
|
||||||
const game = await gameRepository.findOne({
|
const game = await gameRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
status: GameStatus.Cancelled,
|
status: In(["removed", "complete"]),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
import { searchRepacks } from "../helpers/search-games";
|
import { searchRepacks } from "../helpers/search-games";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { GameStatus } from "@shared";
|
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
|
||||||
const getLibrary = async () =>
|
const getLibrary = async () =>
|
||||||
|
@ -24,7 +23,7 @@ const getLibrary = async () =>
|
||||||
...game,
|
...game,
|
||||||
repacks: searchRepacks(game.title),
|
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 { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { gameRepository } from "../../repository";
|
||||||
import { GameStatus } from "@shared";
|
|
||||||
|
|
||||||
const removeGame = async (
|
const removeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -9,10 +8,9 @@ const removeGame = async (
|
||||||
await gameRepository.update(
|
await gameRepository.update(
|
||||||
{
|
{
|
||||||
id: gameId,
|
id: gameId,
|
||||||
status: GameStatus.Cancelled,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: null,
|
status: "removed",
|
||||||
downloadPath: null,
|
downloadPath: null,
|
||||||
bytesDownloaded: 0,
|
bytesDownloaded: 0,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
|
|
|
@ -1,53 +1,25 @@
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { WindowManager } from "@main/services";
|
|
||||||
|
|
||||||
import { In } from "typeorm";
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { GameStatus } from "@shared";
|
|
||||||
|
|
||||||
const cancelGameDownload = async (
|
const cancelGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
await DownloadManager.cancelDownload(gameId);
|
||||||
where: {
|
|
||||||
|
await gameRepository.update(
|
||||||
|
{
|
||||||
id: gameId,
|
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,
|
status: "removed",
|
||||||
bytesDownloaded: 0,
|
bytesDownloaded: 0,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
.then((result) => {
|
|
||||||
if (
|
|
||||||
game.status !== GameStatus.Paused &&
|
|
||||||
game.status !== GameStatus.Seeding
|
|
||||||
) {
|
|
||||||
if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||||
|
|
|
@ -1,30 +1,13 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { gameRepository } from "../../repository";
|
||||||
import { In } from "typeorm";
|
import { DownloadManager } from "@main/services";
|
||||||
import { DownloadManager, WindowManager } from "@main/services";
|
|
||||||
import { GameStatus } from "@shared";
|
|
||||||
|
|
||||||
const pauseGameDownload = async (
|
const pauseGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
DownloadManager.pauseDownload();
|
await DownloadManager.pauseDownload();
|
||||||
|
await gameRepository.update({ id: gameId }, { status: "paused" });
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { Not } from "typeorm";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { gameRepository } from "../../repository";
|
||||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
|
||||||
import { In } from "typeorm";
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { GameStatus } from "@shared";
|
import { dataSource } from "@main/data-source";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
const resumeGameDownload = async (
|
const resumeGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -18,31 +20,21 @@ const resumeGameDownload = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
DownloadManager.pauseDownload();
|
|
||||||
|
|
||||||
if (game.status === GameStatus.Paused) {
|
if (game.status === "paused") {
|
||||||
const downloadsPath = game.downloadPath ?? (await getDownloadsPath());
|
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(
|
await DownloadManager.resumeDownload(game);
|
||||||
{
|
|
||||||
status: In([
|
|
||||||
GameStatus.Downloading,
|
|
||||||
GameStatus.DownloadingMetadata,
|
|
||||||
GameStatus.CheckingFiles,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
{ status: GameStatus.Paused }
|
|
||||||
);
|
|
||||||
|
|
||||||
await gameRepository.update(
|
await transactionalEntityManager
|
||||||
{ id: game.id },
|
.getRepository(Game)
|
||||||
{
|
.update({ id: gameId }, { status: "active" });
|
||||||
status: GameStatus.Downloading,
|
});
|
||||||
downloadPath: downloadsPath,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,39 +1,26 @@
|
||||||
import {
|
import { gameRepository, repackRepository } from "@main/repository";
|
||||||
gameRepository,
|
|
||||||
repackRepository,
|
|
||||||
userPreferencesRepository,
|
|
||||||
} from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||||
import { In } from "typeorm";
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { Downloader, GameStatus } from "@shared";
|
|
||||||
import { stateManager } from "@main/state-manager";
|
import { stateManager } from "@main/state-manager";
|
||||||
|
import { Not } from "typeorm";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
repackId: number,
|
payload: StartGameDownloadPayload
|
||||||
objectID: string,
|
|
||||||
title: string,
|
|
||||||
gameShop: GameShop,
|
|
||||||
downloadPath: string
|
|
||||||
) => {
|
) => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const { repackId, objectID, title, shop, downloadPath, downloader } = payload;
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloader = userPreferences?.realDebridApiToken
|
|
||||||
? Downloader.RealDebrid
|
|
||||||
: Downloader.Torrent;
|
|
||||||
|
|
||||||
const [game, repack] = await Promise.all([
|
const [game, repack] = await Promise.all([
|
||||||
gameRepository.findOne({
|
gameRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
objectID,
|
objectID,
|
||||||
|
shop,
|
||||||
},
|
},
|
||||||
|
relations: { repack: true },
|
||||||
}),
|
}),
|
||||||
repackRepository.findOne({
|
repackRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -42,18 +29,13 @@ const startGameDownload = async (
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!repack || game?.status === GameStatus.Downloading) return;
|
if (!repack) return;
|
||||||
DownloadManager.pauseDownload();
|
|
||||||
|
await DownloadManager.pauseDownload();
|
||||||
|
|
||||||
await gameRepository.update(
|
await gameRepository.update(
|
||||||
{
|
{ status: "active", progress: Not(1) },
|
||||||
status: In([
|
{ status: "paused" }
|
||||||
GameStatus.Downloading,
|
|
||||||
GameStatus.DownloadingMetadata,
|
|
||||||
GameStatus.CheckingFiles,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
{ status: GameStatus.Paused }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
|
@ -62,19 +44,15 @@ const startGameDownload = async (
|
||||||
id: game.id,
|
id: game.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: GameStatus.DownloadingMetadata,
|
status: "active",
|
||||||
downloadPath: downloadPath,
|
progress: 0,
|
||||||
|
bytesDownloaded: 0,
|
||||||
|
downloadPath,
|
||||||
downloader,
|
downloader,
|
||||||
repack: { id: repackId },
|
repack: { id: repackId },
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
DownloadManager.downloadGame(game.id);
|
|
||||||
|
|
||||||
game.status = GameStatus.DownloadingMetadata;
|
|
||||||
|
|
||||||
return game;
|
|
||||||
} else {
|
} else {
|
||||||
const steamGame = stateManager
|
const steamGame = stateManager
|
||||||
.getValue("steamGames")
|
.getValue("steamGames")
|
||||||
|
@ -84,14 +62,14 @@ const startGameDownload = async (
|
||||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const createdGame = await gameRepository
|
await gameRepository
|
||||||
.save({
|
.insert({
|
||||||
title,
|
title,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
objectID,
|
objectID,
|
||||||
downloader,
|
downloader,
|
||||||
shop: gameShop,
|
shop,
|
||||||
status: GameStatus.Downloading,
|
status: "active",
|
||||||
downloadPath,
|
downloadPath,
|
||||||
repack: { id: repackId },
|
repack: { id: repackId },
|
||||||
})
|
})
|
||||||
|
@ -104,13 +82,16 @@ const startGameDownload = async (
|
||||||
|
|
||||||
return result;
|
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);
|
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 { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import { RealDebridClient } from "@main/services/real-debrid";
|
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) => {
|
) =>
|
||||||
if (preferences.realDebridApiToken) {
|
userPreferencesRepository.upsert(
|
||||||
RealDebridClient.authorize(preferences.realDebridApiToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await userPreferencesRepository.upsert(
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
...preferences,
|
...preferences,
|
||||||
},
|
},
|
||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("updateUserPreferences", updateUserPreferences);
|
registerEvent("updateUserPreferences", updateUserPreferences);
|
||||||
|
|
|
@ -85,5 +85,8 @@ export const steamUrlBuilder = {
|
||||||
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
|
`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 "./formatters";
|
||||||
export * from "./ps";
|
export * from "./ps";
|
||||||
|
|
|
@ -3,7 +3,12 @@ import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
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 { dataSource } from "@main/data-source";
|
||||||
import * as resources from "@locales";
|
import * as resources from "@locales";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
|
@ -100,6 +105,10 @@ app.on("window-all-closed", () => {
|
||||||
WindowManager.mainWindow = null;
|
WindowManager.mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
DownloadManager.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
// On OS X it's common to re-create a window in the app when the
|
// 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.
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
|
|
@ -5,27 +5,25 @@ import {
|
||||||
getNewRepacksFromUser,
|
getNewRepacksFromUser,
|
||||||
getNewRepacksFromXatab,
|
getNewRepacksFromXatab,
|
||||||
getNewRepacksFromOnlineFix,
|
getNewRepacksFromOnlineFix,
|
||||||
startProcessWatcher,
|
|
||||||
DownloadManager,
|
DownloadManager,
|
||||||
|
startMainLoop,
|
||||||
} from "./services";
|
} from "./services";
|
||||||
import {
|
import {
|
||||||
gameRepository,
|
gameRepository,
|
||||||
repackRepository,
|
repackRepository,
|
||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
} from "./repository";
|
} from "./repository";
|
||||||
import { TorrentDownloader } from "./services";
|
|
||||||
import { Repack, UserPreferences } from "./entity";
|
import { Repack, UserPreferences } from "./entity";
|
||||||
import { Notification } from "electron";
|
import { Notification } from "electron";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { GameStatus } from "@shared";
|
|
||||||
import { In } from "typeorm";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/real-debrid";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { SteamGame } from "@types";
|
import { SteamGame } from "@types";
|
||||||
|
import { Not } from "typeorm";
|
||||||
|
|
||||||
startProcessWatcher();
|
startMainLoop();
|
||||||
|
|
||||||
const track1337xUsers = async (existingRepacks: Repack[]) => {
|
const track1337xUsers = async (existingRepacks: Repack[]) => {
|
||||||
for (const repacker of repackersOn1337x) {
|
for (const repacker of repackersOn1337x) {
|
||||||
|
@ -72,7 +70,7 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
const repacks = await repackRepository.find({
|
const repacks = repackRepository.find({
|
||||||
order: {
|
order: {
|
||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
},
|
},
|
||||||
|
@ -82,31 +80,24 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
|
fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
|
||||||
) as SteamGame[];
|
) as SteamGame[];
|
||||||
|
|
||||||
stateManager.setValue("repacks", repacks);
|
stateManager.setValue("repacks", await repacks);
|
||||||
stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc"));
|
stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc"));
|
||||||
|
|
||||||
import("./events");
|
import("./events");
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken)
|
if (userPreferences?.realDebridApiToken)
|
||||||
await RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
const game = await gameRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
status: In([
|
status: "active",
|
||||||
GameStatus.Downloading,
|
progress: Not(1),
|
||||||
GameStatus.DownloadingMetadata,
|
|
||||||
GameStatus.CheckingFiles,
|
|
||||||
]),
|
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
relations: { repack: true },
|
relations: { repack: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
await TorrentDownloader.startClient();
|
if (game) DownloadManager.startDownload(game);
|
||||||
|
|
||||||
if (game) {
|
|
||||||
DownloadManager.resumeDownload(game.id);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
userPreferencesRepository
|
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 { Downloader } from "@shared";
|
||||||
|
import { DownloadProgress } from "@types";
|
||||||
import { writePipe } from "./fifo";
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||||
import { RealDebridDownloader } from "./downloaders";
|
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 {
|
export class DownloadManager {
|
||||||
private static gameDownloading: Game;
|
private static downloads = new Map<number, string>();
|
||||||
|
|
||||||
static async getGame(gameId: number) {
|
private static connected = false;
|
||||||
return gameRepository.findOne({
|
private static gid: string | null = null;
|
||||||
where: { id: gameId, isDeleted: false },
|
private static game: Game | null = null;
|
||||||
relations: {
|
private static realDebridTorrentId: string | null = null;
|
||||||
repack: true,
|
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() {
|
public static disconnect() {
|
||||||
if (
|
if (this.aria2c) {
|
||||||
this.gameDownloading &&
|
this.aria2c.kill();
|
||||||
this.gameDownloading.downloader === Downloader.Torrent
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getETA(
|
||||||
|
totalLength: number,
|
||||||
|
completedLength: number,
|
||||||
|
speed: number
|
||||||
) {
|
) {
|
||||||
writePipe.write({ action: "cancel" });
|
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 {
|
} else {
|
||||||
RealDebridDownloader.destroy();
|
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() {
|
static async pauseDownload() {
|
||||||
if (
|
if (this.gid) {
|
||||||
this.gameDownloading &&
|
await this.aria2.call("forcePause", this.gid);
|
||||||
this.gameDownloading.downloader === Downloader.Torrent
|
this.gid = null;
|
||||||
) {
|
}
|
||||||
writePipe.write({ action: "pause" });
|
|
||||||
|
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 {
|
} else {
|
||||||
RealDebridDownloader.destroy();
|
return this.startDownload(game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(gameId: number) {
|
static async startDownload(game: Game) {
|
||||||
const game = await this.getGame(gameId);
|
if (!this.connected) await this.connect();
|
||||||
|
|
||||||
if (game!.downloader === Downloader.Torrent) {
|
const options = {
|
||||||
writePipe.write({
|
dir: game.downloadPath!,
|
||||||
action: "start",
|
};
|
||||||
game_id: game!.id,
|
|
||||||
magnet: game!.repack.magnet,
|
if (game.downloader === Downloader.RealDebrid) {
|
||||||
save_path: game!.downloadPath,
|
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
|
||||||
});
|
game!.repack.magnet
|
||||||
|
);
|
||||||
} else {
|
} 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!;
|
this.game = 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!;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 "./steam-grid";
|
||||||
export * from "./update-resolver";
|
export * from "./update-resolver";
|
||||||
export * from "./window-manager";
|
export * from "./window-manager";
|
||||||
export * from "./fifo";
|
|
||||||
export * from "./downloaders";
|
|
||||||
export * from "./download-manager";
|
export * from "./download-manager";
|
||||||
export * from "./how-long-to-beat";
|
export * from "./how-long-to-beat";
|
||||||
export * from "./process-watcher";
|
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,14 +5,9 @@ import { gameRepository } from "@main/repository";
|
||||||
import { getProcesses } from "@main/helpers";
|
import { getProcesses } from "@main/helpers";
|
||||||
import { WindowManager } from "./window-manager";
|
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 () => {
|
export const watchProcesses = async () => {
|
||||||
const sleepTime = 500;
|
|
||||||
const gamesPlaytime = new Map<number, number>();
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
const games = await gameRepository.find({
|
const games = await gameRepository.find({
|
||||||
where: {
|
where: {
|
||||||
executablePath: Not(IsNull()),
|
executablePath: Not(IsNull()),
|
||||||
|
@ -20,10 +15,7 @@ export const startProcessWatcher = async () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (games.length === 0) {
|
if (games.length === 0) return;
|
||||||
await sleep(sleepTime);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const processes = await getProcesses();
|
const processes = await getProcesses();
|
||||||
|
|
||||||
|
@ -40,9 +32,7 @@ export const startProcessWatcher = async () => {
|
||||||
return runningProcess.name === basename;
|
return runningProcess.name === basename;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [basename, basenameWithoutExtension].includes(
|
return [basename, basenameWithoutExtension].includes(runningProcess.name);
|
||||||
runningProcess.name
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (gameProcess) {
|
if (gameProcess) {
|
||||||
|
@ -63,12 +53,10 @@ export const startProcessWatcher = async () => {
|
||||||
gamesPlaytime.set(game.id, performance.now());
|
gamesPlaytime.set(game.id, performance.now());
|
||||||
} else if (gamesPlaytime.has(game.id)) {
|
} else if (gamesPlaytime.has(game.id)) {
|
||||||
gamesPlaytime.delete(game.id);
|
gamesPlaytime.delete(game.id);
|
||||||
|
|
||||||
if (WindowManager.mainWindow) {
|
if (WindowManager.mainWindow) {
|
||||||
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
|
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 {
|
import type {
|
||||||
RealDebridAddMagnet,
|
RealDebridAddMagnet,
|
||||||
RealDebridTorrentInfo,
|
RealDebridTorrentInfo,
|
||||||
RealDebridUnrestrictLink,
|
RealDebridUnrestrictLink,
|
||||||
} from "./real-debrid.types";
|
RealDebridUser,
|
||||||
import axios, { AxiosInstance } from "axios";
|
} from "@types";
|
||||||
|
|
||||||
const base = "https://api.real-debrid.com/rest/1.0";
|
|
||||||
|
|
||||||
export class RealDebridClient {
|
export class RealDebridClient {
|
||||||
private static instance: AxiosInstance;
|
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) {
|
static async addMagnet(magnet: string) {
|
||||||
const searchParams = new URLSearchParams({ magnet });
|
const searchParams = new URLSearchParams({ magnet });
|
||||||
|
@ -22,13 +31,18 @@ export class RealDebridClient {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getInfo(id: string) {
|
static async getTorrentInfo(id: string) {
|
||||||
const response = await this.instance.get<RealDebridTorrentInfo>(
|
const response = await this.instance.get<RealDebridTorrentInfo>(
|
||||||
`/torrents/info/${id}`
|
`/torrents/info/${id}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getUser() {
|
||||||
|
const response = await this.instance.get<RealDebridUser>(`/user`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
static async selectAllFiles(id: string) {
|
static async selectAllFiles(id: string) {
|
||||||
const searchParams = new URLSearchParams({ files: "all" });
|
const searchParams = new URLSearchParams({ files: "all" });
|
||||||
|
|
||||||
|
@ -49,51 +63,24 @@ export class RealDebridClient {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getAllTorrentsFromUser() {
|
private static async getAllTorrentsFromUser() {
|
||||||
const response =
|
const response =
|
||||||
await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
|
await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static extractSHA1FromMagnet(magnet: string) {
|
static async getTorrentId(magnetUri: string) {
|
||||||
return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase();
|
const userTorrents = await RealDebridClient.getAllTorrentsFromUser();
|
||||||
}
|
|
||||||
|
|
||||||
static async getDownloadUrl(game: Game) {
|
const { infoHash } = await parseTorrent(magnetUri);
|
||||||
const torrents = await RealDebridClient.getAllTorrentsFromUser();
|
const userTorrent = userTorrents.find(
|
||||||
const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet);
|
(userTorrent) => userTorrent.hash === infoHash
|
||||||
let torrent = torrents.find((t) => t.hash === hash);
|
);
|
||||||
|
|
||||||
if (!torrent) {
|
if (userTorrent) return userTorrent.id;
|
||||||
const magnet = await RealDebridClient.addMagnet(game!.repack.magnet);
|
|
||||||
|
|
||||||
if (magnet && magnet.id) {
|
const torrent = await RealDebridClient.addMagnet(magnetUri);
|
||||||
await RealDebridClient.selectAllFiles(magnet.id);
|
return torrent.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}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -99,7 +99,7 @@ export class WindowManager {
|
||||||
},
|
},
|
||||||
take: 5,
|
take: 5,
|
||||||
order: {
|
order: {
|
||||||
updatedAt: "DESC",
|
lastTimePlayed: "DESC",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,38 +5,26 @@ import { contextBridge, ipcRenderer } from "electron";
|
||||||
import type {
|
import type {
|
||||||
CatalogueCategory,
|
CatalogueCategory,
|
||||||
GameShop,
|
GameShop,
|
||||||
TorrentProgress,
|
DownloadProgress,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
AppUpdaterEvents,
|
AppUpdaterEvent,
|
||||||
|
StartGameDownloadPayload,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
startGameDownload: (
|
startGameDownload: (payload: StartGameDownloadPayload) =>
|
||||||
repackId: number,
|
ipcRenderer.invoke("startGameDownload", payload),
|
||||||
objectID: string,
|
|
||||||
title: string,
|
|
||||||
shop: GameShop,
|
|
||||||
downloadPath: string
|
|
||||||
) =>
|
|
||||||
ipcRenderer.invoke(
|
|
||||||
"startGameDownload",
|
|
||||||
repackId,
|
|
||||||
objectID,
|
|
||||||
title,
|
|
||||||
shop,
|
|
||||||
downloadPath
|
|
||||||
),
|
|
||||||
cancelGameDownload: (gameId: number) =>
|
cancelGameDownload: (gameId: number) =>
|
||||||
ipcRenderer.invoke("cancelGameDownload", gameId),
|
ipcRenderer.invoke("cancelGameDownload", gameId),
|
||||||
pauseGameDownload: (gameId: number) =>
|
pauseGameDownload: (gameId: number) =>
|
||||||
ipcRenderer.invoke("pauseGameDownload", gameId),
|
ipcRenderer.invoke("pauseGameDownload", gameId),
|
||||||
resumeGameDownload: (gameId: number) =>
|
resumeGameDownload: (gameId: number) =>
|
||||||
ipcRenderer.invoke("resumeGameDownload", gameId),
|
ipcRenderer.invoke("resumeGameDownload", gameId),
|
||||||
onDownloadProgress: (cb: (value: TorrentProgress) => void) => {
|
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
value: TorrentProgress
|
value: DownloadProgress
|
||||||
) => cb(value);
|
) => cb(value);
|
||||||
ipcRenderer.on("on-download-progress", listener);
|
ipcRenderer.on("on-download-progress", listener);
|
||||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||||
|
@ -61,6 +49,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
updateUserPreferences: (preferences: UserPreferences) =>
|
updateUserPreferences: (preferences: UserPreferences) =>
|
||||||
ipcRenderer.invoke("updateUserPreferences", preferences),
|
ipcRenderer.invoke("updateUserPreferences", preferences),
|
||||||
autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
|
autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
|
||||||
|
authenticateRealDebrid: (apiToken: string) =>
|
||||||
|
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
|
@ -84,6 +74,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
||||||
removeGameFromLibrary: (gameId: number) =>
|
removeGameFromLibrary: (gameId: number) =>
|
||||||
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
||||||
|
removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId),
|
||||||
deleteGameFolder: (gameId: number) =>
|
deleteGameFolder: (gameId: number) =>
|
||||||
ipcRenderer.invoke("deleteGameFolder", gameId),
|
ipcRenderer.invoke("deleteGameFolder", gameId),
|
||||||
getGameByObjectID: (objectID: string) =>
|
getGameByObjectID: (objectID: string) =>
|
||||||
|
@ -115,10 +106,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
|
||||||
/* Auto update */
|
/* Auto update */
|
||||||
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => {
|
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvent) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
value: AppUpdaterEvents
|
value: AppUpdaterEvent
|
||||||
) => cb(value);
|
) => cb(value);
|
||||||
|
|
||||||
ipcRenderer.on("autoUpdaterEvent", listener);
|
ipcRenderer.on("autoUpdaterEvent", listener);
|
||||||
|
|
|
@ -26,9 +26,9 @@ globalStyle("body", {
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
fontFamily: "'Fira Mono', monospace",
|
fontFamily: "'Fira Mono', monospace",
|
||||||
fontSize: vars.size.bodyFontSize,
|
fontSize: vars.size.body,
|
||||||
background: vars.color.background,
|
background: vars.color.background,
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
margin: "0",
|
margin: "0",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ globalStyle(
|
||||||
);
|
);
|
||||||
|
|
||||||
globalStyle("label", {
|
globalStyle("label", {
|
||||||
fontSize: vars.size.bodyFontSize,
|
fontSize: vars.size.body,
|
||||||
});
|
});
|
||||||
|
|
||||||
globalStyle("input[type=number]", {
|
globalStyle("input[type=number]", {
|
||||||
|
@ -79,6 +79,10 @@ globalStyle("img", {
|
||||||
WebkitUserDrag: "none",
|
WebkitUserDrag: "none",
|
||||||
} as Record<string, string>);
|
} as Record<string, string>);
|
||||||
|
|
||||||
|
globalStyle("progress[value]", {
|
||||||
|
WebkitAppearance: "none",
|
||||||
|
});
|
||||||
|
|
||||||
export const container = style({
|
export const container = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { Sidebar, BottomPanel, Header } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
|
@ -18,8 +18,8 @@ import {
|
||||||
clearSearch,
|
clearSearch,
|
||||||
setUserPreferences,
|
setUserPreferences,
|
||||||
toggleDraggingDisabled,
|
toggleDraggingDisabled,
|
||||||
|
closeToast,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { GameStatusHelper } from "@shared";
|
|
||||||
|
|
||||||
document.body.classList.add(themeClass);
|
document.body.classList.add(themeClass);
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ export function App() {
|
||||||
const draggingDisabled = useAppSelector(
|
const draggingDisabled = useAppSelector(
|
||||||
(state) => state.window.draggingDisabled
|
(state) => state.window.draggingDisabled
|
||||||
);
|
);
|
||||||
|
const toast = useAppSelector((state) => state.toast);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
|
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
|
||||||
|
@ -54,7 +55,7 @@ export function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onDownloadProgress(
|
const unsubscribe = window.electron.onDownloadProgress(
|
||||||
(downloadProgress) => {
|
(downloadProgress) => {
|
||||||
if (GameStatusHelper.isReady(downloadProgress.game.status)) {
|
if (downloadProgress.game.progress === 1) {
|
||||||
clearDownload();
|
clearDownload();
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
return;
|
return;
|
||||||
|
@ -109,6 +110,10 @@ export function App() {
|
||||||
});
|
});
|
||||||
}, [dispatch, draggingDisabled]);
|
}, [dispatch, draggingDisabled]);
|
||||||
|
|
||||||
|
const handleToastClose = useCallback(() => {
|
||||||
|
dispatch(closeToast());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{window.electron.platform === "win32" && (
|
{window.electron.platform === "win32" && (
|
||||||
|
@ -132,7 +137,15 @@ export function App() {
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomPanel />
|
<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)",
|
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) {
|
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||||
return (
|
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({
|
export const bottomPanel = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
borderTop: `solid 1px ${vars.color.border}`,
|
borderTop: `solid 1px ${vars.color.border}`,
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
|
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
position: "relative",
|
||||||
zIndex: "1",
|
zIndex: "1",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadsButton = style({
|
export const downloadsButton = style({
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
borderBottom: "1px solid transparent",
|
borderBottom: "1px solid transparent",
|
||||||
":hover": {
|
":hover": {
|
||||||
borderBottom: `1px solid ${vars.color.bodyText}`,
|
borderBottom: `1px solid ${vars.color.body}`,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useDownload } from "@renderer/hooks";
|
import { useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./bottom-panel.css";
|
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 { useNavigate } from "react-router-dom";
|
||||||
import { VERSION_CODENAME } from "@renderer/constants";
|
import { VERSION_CODENAME } from "@renderer/constants";
|
||||||
import { GameStatus, GameStatusHelper } from "@shared";
|
|
||||||
|
|
||||||
export function BottomPanel() {
|
export function BottomPanel() {
|
||||||
const { t } = useTranslation("bottom_panel");
|
const { t } = useTranslation("bottom_panel");
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { game, progress, downloadSpeed, eta } = useDownload();
|
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||||
|
|
||||||
const isGameDownloading =
|
const isGameDownloading = !!lastPacket?.game;
|
||||||
game && GameStatusHelper.isDownloading(game.status ?? null);
|
|
||||||
|
|
||||||
const [version, setVersion] = useState("");
|
const [version, setVersion] = useState("");
|
||||||
|
|
||||||
|
@ -27,17 +25,18 @@ export function BottomPanel() {
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
if (isGameDownloading) {
|
if (isGameDownloading) {
|
||||||
if (game.status === GameStatus.DownloadingMetadata)
|
if (lastPacket?.isDownloadingMetadata)
|
||||||
return t("downloading_metadata", { title: game.title });
|
return t("downloading_metadata", { title: lastPacket?.game.title });
|
||||||
|
|
||||||
if (game.status === GameStatus.CheckingFiles)
|
if (!eta) {
|
||||||
return t("checking_files", {
|
return t("calculating_eta", {
|
||||||
title: game.title,
|
title: lastPacket?.game.title,
|
||||||
percentage: progress,
|
percentage: progress,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return t("downloading", {
|
return t("downloading", {
|
||||||
title: game?.title,
|
title: lastPacket?.game.title,
|
||||||
percentage: progress,
|
percentage: progress,
|
||||||
eta,
|
eta,
|
||||||
speed: downloadSpeed,
|
speed: downloadSpeed,
|
||||||
|
@ -45,17 +44,18 @@ export function BottomPanel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return t("no_downloads_in_progress");
|
return t("no_downloads_in_progress");
|
||||||
}, [t, isGameDownloading, game, progress, eta, downloadSpeed]);
|
}, [
|
||||||
|
t,
|
||||||
|
isGameDownloading,
|
||||||
|
lastPacket?.game,
|
||||||
|
lastPacket?.isDownloadingMetadata,
|
||||||
|
progress,
|
||||||
|
eta,
|
||||||
|
downloadSpeed,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer className={styles.bottomPanel}>
|
||||||
className={styles.bottomPanel}
|
|
||||||
style={{
|
|
||||||
background: isGameDownloading
|
|
||||||
? `linear-gradient(90deg, ${vars.color.background} ${progress}, ${vars.color.darkBackground} ${progress})`
|
|
||||||
: vars.color.darkBackground,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.downloadsButton}
|
className={styles.downloadsButton}
|
||||||
|
|
|
@ -18,7 +18,6 @@ const base = style({
|
||||||
},
|
},
|
||||||
":disabled": {
|
":disabled": {
|
||||||
opacity: vars.opacity.disabled,
|
opacity: vars.opacity.disabled,
|
||||||
pointerEvents: "none",
|
|
||||||
cursor: "not-allowed",
|
cursor: "not-allowed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -30,6 +29,9 @@ export const button = styleVariants({
|
||||||
":hover": {
|
":hover": {
|
||||||
backgroundColor: "#DADBE1",
|
backgroundColor: "#DADBE1",
|
||||||
},
|
},
|
||||||
|
":disabled": {
|
||||||
|
backgroundColor: vars.color.muted,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
outline: [
|
outline: [
|
||||||
|
@ -41,6 +43,9 @@ export const button = styleVariants({
|
||||||
":hover": {
|
":hover": {
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||||
},
|
},
|
||||||
|
":disabled": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dark: [
|
dark: [
|
||||||
|
|
|
@ -109,6 +109,6 @@ export const shopIcon = style({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const noDownloadsLabel = style({
|
export const noDownloadsLabel = style({
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||||
import type { CatalogueEntry } from "@types";
|
import type { CatalogueEntry } from "@types";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
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 * as styles from "./game-card.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -16,7 +15,6 @@ export interface GameCardProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const shopIcon = {
|
const shopIcon = {
|
||||||
epic: <EpicGamesLogo className={styles.shopIcon} />,
|
|
||||||
steam: <SteamLogo className={styles.shopIcon} />,
|
steam: <SteamLogo className={styles.shopIcon} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ export const section = style({
|
||||||
|
|
||||||
export const backButton = recipe({
|
export const backButton = recipe({
|
||||||
base: {
|
base: {
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
WebkitAppRegion: "no-drag",
|
WebkitAppRegion: "no-drag",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
@ -156,10 +156,10 @@ export const newVersionButton = style({
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
borderBottom: "1px solid transparent",
|
borderBottom: "1px solid transparent",
|
||||||
":hover": {
|
":hover": {
|
||||||
borderBottom: `1px solid ${vars.color.bodyText}`,
|
borderBottom: `1px solid ${vars.color.body}`,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -80,7 +80,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
|
|
|
@ -8,3 +8,4 @@ export * from "./sidebar/sidebar";
|
||||||
export * from "./text-field/text-field";
|
export * from "./text-field/text-field";
|
||||||
export * from "./checkbox-field/checkbox-field";
|
export * from "./checkbox-field/checkbox-field";
|
||||||
export * from "./link/link";
|
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 { recipe } from "@vanilla-extract/recipes";
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const modalSlideIn = keyframes({
|
export const fadeIn = keyframes({
|
||||||
"0%": { opacity: 0 },
|
"0%": { opacity: 0 },
|
||||||
"100%": {
|
"100%": {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const modalSlideOut = keyframes({
|
export const fadeOut = keyframes({
|
||||||
"0%": { opacity: 1 },
|
"0%": { opacity: 1 },
|
||||||
"100%": {
|
"100%": {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
@ -18,12 +18,12 @@ export const modalSlideOut = keyframes({
|
||||||
|
|
||||||
export const modal = recipe({
|
export const modal = recipe({
|
||||||
base: {
|
base: {
|
||||||
animationName: modalSlideIn,
|
animationName: fadeIn,
|
||||||
animationDuration: "0.3s",
|
animationDuration: "0.3s",
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
maxWidth: "600px",
|
maxWidth: "600px",
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px ${vars.color.border}`,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
@ -33,7 +33,7 @@ export const modal = recipe({
|
||||||
variants: {
|
variants: {
|
||||||
closing: {
|
closing: {
|
||||||
true: {
|
true: {
|
||||||
animationName: modalSlideOut,
|
animationName: fadeOut,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -65,5 +65,5 @@ export const closeModalButton = style({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const closeModalButtonIcon = style({
|
export const closeModalButtonIcon = style({
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const content = recipe({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
paddingBottom: "0",
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
},
|
},
|
||||||
|
@ -118,7 +118,6 @@ export const sectionTitle = style({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const section = style({
|
export const section = style({
|
||||||
padding: `${SPACING_UNIT * 2}px 0`,
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { GameStatus, GameStatusHelper } from "@shared";
|
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
@ -35,14 +34,14 @@ export function Sidebar() {
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const { game: gameDownloading, progress } = useDownload();
|
const { lastPacket, progress } = useDownload();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [gameDownloading?.id, updateLibrary]);
|
}, [lastPacket?.game.id, updateLibrary]);
|
||||||
|
|
||||||
const isDownloading = library.some((game) =>
|
const isDownloading = library.some(
|
||||||
GameStatusHelper.isDownloading(game.status)
|
(game) => game.status === "active" && game.progress !== 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const sidebarRef = useRef<HTMLElement>(null);
|
const sidebarRef = useRef<HTMLElement>(null);
|
||||||
|
@ -101,18 +100,9 @@ export function Sidebar() {
|
||||||
}, [isResizing]);
|
}, [isResizing]);
|
||||||
|
|
||||||
const getGameTitle = (game: Game) => {
|
const getGameTitle = (game: Game) => {
|
||||||
if (game.status === GameStatus.Paused)
|
if (game.status === "paused") return t("paused", { title: game.title });
|
||||||
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 (lastPacket?.game.id === game.id) {
|
||||||
return t("downloading", {
|
return t("downloading", {
|
||||||
title: game.title,
|
title: game.title,
|
||||||
percentage: progress,
|
percentage: progress,
|
||||||
|
@ -183,7 +173,7 @@ export function Sidebar() {
|
||||||
className={styles.menuItem({
|
className={styles.menuItem({
|
||||||
active:
|
active:
|
||||||
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||||
muted: game.status === GameStatus.Cancelled,
|
muted: game.status === "removed",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<button
|
<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 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,
|
HowLongToBeatCategory,
|
||||||
ShopDetails,
|
ShopDetails,
|
||||||
Steam250Game,
|
Steam250Game,
|
||||||
TorrentProgress,
|
DownloadProgress,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
|
StartGameDownloadPayload,
|
||||||
|
RealDebridUser,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
|
@ -21,18 +23,12 @@ declare global {
|
||||||
|
|
||||||
interface Electron {
|
interface Electron {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
startGameDownload: (
|
startGameDownload: (payload: StartGameDownloadPayload) => Promise<void>;
|
||||||
repackId: number,
|
|
||||||
objectID: string,
|
|
||||||
title: string,
|
|
||||||
shop: GameShop,
|
|
||||||
downloadPath: string
|
|
||||||
) => Promise<Game>;
|
|
||||||
cancelGameDownload: (gameId: number) => Promise<void>;
|
cancelGameDownload: (gameId: number) => Promise<void>;
|
||||||
pauseGameDownload: (gameId: number) => Promise<void>;
|
pauseGameDownload: (gameId: number) => Promise<void>;
|
||||||
resumeGameDownload: (gameId: number) => Promise<void>;
|
resumeGameDownload: (gameId: number) => Promise<void>;
|
||||||
onDownloadProgress: (
|
onDownloadProgress: (
|
||||||
cb: (value: TorrentProgress) => void
|
cb: (value: DownloadProgress) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
|
@ -67,6 +63,7 @@ declare global {
|
||||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||||
closeGame: (gameId: number) => Promise<boolean>;
|
closeGame: (gameId: number) => Promise<boolean>;
|
||||||
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
||||||
|
removeGame: (gameId: number) => Promise<void>;
|
||||||
deleteGameFolder: (gameId: number) => Promise<unknown>;
|
deleteGameFolder: (gameId: number) => Promise<unknown>;
|
||||||
getGameByObjectID: (objectID: string) => Promise<Game | null>;
|
getGameByObjectID: (objectID: string) => Promise<Game | null>;
|
||||||
onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer;
|
onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer;
|
||||||
|
@ -78,6 +75,7 @@ declare global {
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
autoLaunch: (enabled: boolean) => Promise<void>;
|
autoLaunch: (enabled: boolean) => Promise<void>;
|
||||||
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
|
|
||||||
/* Hardware */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||||
import type { TorrentProgress } from "@types";
|
import type { DownloadProgress } from "@types";
|
||||||
|
|
||||||
export interface DownloadState {
|
export interface DownloadState {
|
||||||
lastPacket: TorrentProgress | null;
|
lastPacket: DownloadProgress | null;
|
||||||
gameId: number | null;
|
gameId: number | null;
|
||||||
gamesWithDeletionInProgress: number[];
|
gamesWithDeletionInProgress: number[];
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export const downloadSlice = createSlice({
|
||||||
name: "download",
|
name: "download",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setLastPacket: (state, action: PayloadAction<TorrentProgress>) => {
|
setLastPacket: (state, action: PayloadAction<DownloadProgress>) => {
|
||||||
state.lastPacket = action.payload;
|
state.lastPacket = action.payload;
|
||||||
if (!state.gameId) state.gameId = action.payload.game.id;
|
if (!state.gameId) state.gameId = action.payload.game.id;
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,3 +3,4 @@ export * from "./library-slice";
|
||||||
export * from "./use-preferences-slice";
|
export * from "./use-preferences-slice";
|
||||||
export * from "./download-slice";
|
export * from "./download-slice";
|
||||||
export * from "./window-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-download";
|
||||||
export * from "./use-library";
|
export * from "./use-library";
|
||||||
export * from "./use-date";
|
export * from "./use-date";
|
||||||
|
export * from "./use-toast";
|
||||||
export * from "./redux";
|
export * from "./redux";
|
||||||
|
|
|
@ -9,9 +9,9 @@ import {
|
||||||
setGameDeleting,
|
setGameDeleting,
|
||||||
removeGameFromDeleting,
|
removeGameFromDeleting,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import type { GameShop, TorrentProgress } from "@types";
|
import type { DownloadProgress, StartGameDownloadPayload } from "@types";
|
||||||
import { useDate } from "./use-date";
|
import { useDate } from "./use-date";
|
||||||
import { GameStatus, GameStatusHelper, formatBytes } from "@shared";
|
import { formatBytes } from "@shared";
|
||||||
|
|
||||||
export function useDownload() {
|
export function useDownload() {
|
||||||
const { updateLibrary } = useLibrary();
|
const { updateLibrary } = useLibrary();
|
||||||
|
@ -22,57 +22,54 @@ export function useDownload() {
|
||||||
);
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const startDownload = (
|
const startDownload = (payload: StartGameDownloadPayload) =>
|
||||||
repackId: number,
|
window.electron.startGameDownload(payload).then((game) => {
|
||||||
objectID: string,
|
|
||||||
title: string,
|
|
||||||
shop: GameShop,
|
|
||||||
downloadPath: string
|
|
||||||
) =>
|
|
||||||
window.electron
|
|
||||||
.startGameDownload(repackId, objectID, title, shop, downloadPath)
|
|
||||||
.then((game) => {
|
|
||||||
dispatch(clearDownload());
|
dispatch(clearDownload());
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
});
|
});
|
||||||
|
|
||||||
const pauseDownload = (gameId: number) =>
|
const pauseDownload = async (gameId: number) => {
|
||||||
window.electron.pauseGameDownload(gameId).then(() => {
|
await window.electron.pauseGameDownload(gameId);
|
||||||
|
await updateLibrary();
|
||||||
|
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());
|
dispatch(clearDownload());
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
});
|
};
|
||||||
|
|
||||||
const resumeDownload = (gameId: number) =>
|
const removeGameInstaller = async (gameId: number) => {
|
||||||
window.electron.resumeGameDownload(gameId).then(() => {
|
dispatch(setGameDeleting(gameId));
|
||||||
updateLibrary();
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancelDownload = (gameId: number) =>
|
try {
|
||||||
window.electron.cancelGameDownload(gameId).then(() => {
|
await window.electron.deleteGameFolder(gameId);
|
||||||
dispatch(clearDownload());
|
await window.electron.removeGame(gameId);
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
deleteGame(gameId);
|
} finally {
|
||||||
});
|
dispatch(removeGameFromDeleting(gameId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const removeGameFromLibrary = (gameId: number) =>
|
const removeGameFromLibrary = (gameId: number) =>
|
||||||
window.electron.removeGameFromLibrary(gameId).then(() => {
|
window.electron.removeGameFromLibrary(gameId).then(() => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
});
|
});
|
||||||
|
|
||||||
const isVerifying = GameStatusHelper.isVerifying(
|
|
||||||
lastPacket?.game.status ?? null
|
|
||||||
);
|
|
||||||
|
|
||||||
const getETA = () => {
|
const getETA = () => {
|
||||||
if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) {
|
if (!lastPacket || lastPacket.timeRemaining < 0) return "";
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return formatDistance(
|
return formatDistance(
|
||||||
addMilliseconds(new Date(), lastPacket?.timeRemaining ?? 1),
|
addMilliseconds(new Date(), lastPacket.timeRemaining),
|
||||||
new Date(),
|
new Date(),
|
||||||
{ addSuffix: true }
|
{ 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) => {
|
const isGameDeleting = (gameId: number) => {
|
||||||
return gamesWithDeletionInProgress.includes(gameId);
|
return gamesWithDeletionInProgress.includes(gameId);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
game: lastPacket?.game,
|
|
||||||
bytesDownloaded: lastPacket?.game.bytesDownloaded,
|
|
||||||
fileSize: lastPacket?.game.fileSize,
|
|
||||||
isVerifying,
|
|
||||||
gameId: lastPacket?.game.id,
|
|
||||||
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
|
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
|
||||||
progress: getProgress(),
|
progress: formatDownloadProgress(lastPacket?.game.progress),
|
||||||
numPeers: lastPacket?.numPeers,
|
lastPacket,
|
||||||
numSeeds: lastPacket?.numSeeds,
|
|
||||||
eta: getETA(),
|
eta: getETA(),
|
||||||
startDownload,
|
startDownload,
|
||||||
pauseDownload,
|
pauseDownload,
|
||||||
resumeDownload,
|
resumeDownload,
|
||||||
cancelDownload,
|
cancelDownload,
|
||||||
removeGameFromLibrary,
|
removeGameFromLibrary,
|
||||||
deleteGame,
|
removeGameInstaller,
|
||||||
isGameDeleting,
|
isGameDeleting,
|
||||||
clearDownload: () => dispatch(clearDownload()),
|
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 { 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;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
deleteGame: () => void;
|
deleteGame: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeleteModal({
|
export function DeleteGameModal({
|
||||||
onClose,
|
onClose,
|
||||||
visible,
|
visible,
|
||||||
deleteGame,
|
deleteGame,
|
||||||
}: DeleteModalProps) {
|
}: DeleteGameModalProps) {
|
||||||
const { t } = useTranslation("downloads");
|
const { t } = useTranslation("downloads");
|
||||||
|
|
||||||
const handleDeleteGame = () => {
|
const handleDeleteGame = () => {
|
|
@ -12,7 +12,7 @@ export const downloadTitleWrapper = style({
|
||||||
export const downloadTitle = style({
|
export const downloadTitle = style({
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
display: "block",
|
display: "block",
|
||||||
|
@ -29,7 +29,6 @@ export const downloaderName = style({
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
alignSelf: "flex-start",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloads = style({
|
export const downloads = style({
|
||||||
|
@ -46,9 +45,34 @@ export const downloadCover = style({
|
||||||
width: "280px",
|
width: "280px",
|
||||||
minWidth: "280px",
|
minWidth: "280px",
|
||||||
height: "auto",
|
height: "auto",
|
||||||
objectFit: "cover",
|
|
||||||
objectPosition: "center",
|
|
||||||
borderRight: `solid 1px ${vars.color.border}`,
|
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({
|
export const download = recipe({
|
||||||
|
|
|
@ -2,21 +2,30 @@ import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Button, TextField } from "@renderer/components";
|
import { Button, TextField } from "@renderer/components";
|
||||||
import { formatDownloadProgress, steamUrlBuilder } from "@renderer/helpers";
|
import {
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
buildGameDetailsPath,
|
||||||
|
formatDownloadProgress,
|
||||||
|
steamUrlBuilder,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||||
import * as styles from "./downloads.css";
|
import * as styles from "./downloads.css";
|
||||||
import { DeleteModal } from "./delete-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { Downloader, GameStatus, GameStatusHelper, formatBytes } from "@shared";
|
import { Downloader, formatBytes } from "@shared";
|
||||||
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
|
|
||||||
export function Downloads() {
|
export function Downloads() {
|
||||||
const { library, updateLibrary } = useLibrary();
|
const { library, updateLibrary } = useLibrary();
|
||||||
|
|
||||||
const { t } = useTranslation("downloads");
|
const { t } = useTranslation("downloads");
|
||||||
|
|
||||||
|
const userPreferences = useAppSelector(
|
||||||
|
(state) => state.userPreferences.value
|
||||||
|
);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const gameToBeDeleted = useRef<number | null>(null);
|
const gameToBeDeleted = useRef<number | null>(null);
|
||||||
|
@ -26,15 +35,13 @@ export function Downloads() {
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
game: gameDownloading,
|
lastPacket,
|
||||||
progress,
|
progress,
|
||||||
numPeers,
|
|
||||||
numSeeds,
|
|
||||||
pauseDownload,
|
pauseDownload,
|
||||||
resumeDownload,
|
resumeDownload,
|
||||||
removeGameFromLibrary,
|
removeGameFromLibrary,
|
||||||
cancelDownload,
|
cancelDownload,
|
||||||
deleteGame,
|
removeGameInstaller,
|
||||||
isGameDeleting,
|
isGameDeleting,
|
||||||
} = useDownload();
|
} = useDownload();
|
||||||
|
|
||||||
|
@ -53,27 +60,22 @@ export function Downloads() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const getFinalDownloadSize = (game: Game) => {
|
const getFinalDownloadSize = (game: Game) => {
|
||||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
if (!game) return "N/A";
|
if (!game) return "N/A";
|
||||||
if (game.fileSize) return formatBytes(game.fileSize);
|
if (game.fileSize) return formatBytes(game.fileSize);
|
||||||
|
|
||||||
if (gameDownloading?.fileSize && isGameDownloading)
|
if (lastPacket?.game.fileSize && isGameDownloading)
|
||||||
return formatBytes(gameDownloading.fileSize);
|
return formatBytes(lastPacket?.game.fileSize);
|
||||||
|
|
||||||
return game.repack?.fileSize ?? "N/A";
|
return game.repack?.fileSize ?? "N/A";
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloaderName = {
|
|
||||||
[Downloader.RealDebrid]: t("real_debrid"),
|
|
||||||
[Downloader.Torrent]: t("torrent"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGameInfo = (game: Game) => {
|
const getGameInfo = (game: Game) => {
|
||||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
const finalDownloadSize = getFinalDownloadSize(game);
|
const finalDownloadSize = getFinalDownloadSize(game);
|
||||||
|
|
||||||
if (isGameDeleting(game?.id)) {
|
if (isGameDeleting(game.id)) {
|
||||||
return <p>{t("deleting")}</p>;
|
return <p>{t("deleting")}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,39 +84,30 @@ export function Downloads() {
|
||||||
<>
|
<>
|
||||||
<p>{progress}</p>
|
<p>{progress}</p>
|
||||||
|
|
||||||
{gameDownloading?.status &&
|
|
||||||
gameDownloading?.status !== GameStatus.Downloading ? (
|
|
||||||
<p>{t(gameDownloading?.status)}</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>
|
<p>
|
||||||
{formatBytes(gameDownloading?.bytesDownloaded)} /{" "}
|
{formatBytes(lastPacket?.game.bytesDownloaded)} /{" "}
|
||||||
{finalDownloadSize}
|
{finalDownloadSize}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{game.downloader === Downloader.Torrent && (
|
{game.downloader === Downloader.Torrent && (
|
||||||
<p>
|
<small>
|
||||||
{numPeers} peers / {numSeeds} seeds
|
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||||
</p>
|
</small>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GameStatusHelper.isReady(game?.status)) {
|
if (game.progress === 1) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{game?.repack?.title}</p>
|
<p>{game.repack?.title}</p>
|
||||||
<p>{t("completed")}</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{formatDownloadProgress(game.progress)}</p>
|
<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) => {
|
const openDeleteModal = (gameId: number) => {
|
||||||
|
@ -132,37 +137,11 @@ export function Downloads() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGameActions = (game: Game) => {
|
const getGameActions = (game: Game) => {
|
||||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
const deleting = isGameDeleting(game.id);
|
const deleting = isGameDeleting(game.id);
|
||||||
|
|
||||||
if (isGameDownloading) {
|
if (game.progress === 1) {
|
||||||
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)) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
@ -180,18 +159,43 @@ export function Downloads() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game?.status === GameStatus.DownloadingMetadata) {
|
if (isGameDownloading || game.status === "active") {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||||
|
{t("pause")}
|
||||||
|
</Button>
|
||||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate(`/game/${game.shop}/${game.objectID}`)}
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
>
|
>
|
||||||
|
@ -219,10 +223,9 @@ export function Downloads() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteGame = () => {
|
const handleDeleteGame = async () => {
|
||||||
if (gameToBeDeleted.current) {
|
if (gameToBeDeleted.current)
|
||||||
deleteGame(gameToBeDeleted.current).then(updateLibrary);
|
await removeGameInstaller(gameToBeDeleted.current);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -231,7 +234,8 @@ export function Downloads() {
|
||||||
visible={showBinaryNotFoundModal}
|
visible={showBinaryNotFoundModal}
|
||||||
onClose={() => setShowBinaryNotFoundModal(false)}
|
onClose={() => setShowBinaryNotFoundModal(false)}
|
||||||
/>
|
/>
|
||||||
<DeleteModal
|
|
||||||
|
<DeleteGameModal
|
||||||
visible={showDeleteModal}
|
visible={showDeleteModal}
|
||||||
onClose={() => setShowDeleteModal(false)}
|
onClose={() => setShowDeleteModal(false)}
|
||||||
deleteGame={handleDeleteGame}
|
deleteGame={handleDeleteGame}
|
||||||
|
@ -245,31 +249,36 @@ export function Downloads() {
|
||||||
<li
|
<li
|
||||||
key={game.id}
|
key={game.id}
|
||||||
className={styles.download({
|
className={styles.download({
|
||||||
cancelled: game.status === GameStatus.Cancelled,
|
cancelled: game.status === "removed",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<div className={styles.downloadCover}>
|
||||||
|
<div className={styles.downloadCoverBackdrop}>
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectID)}
|
src={steamUrlBuilder.library(game.objectID)}
|
||||||
className={styles.downloadCover}
|
className={styles.downloadCoverImage}
|
||||||
alt={game.title}
|
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.downloadRightContent}>
|
||||||
<div className={styles.downloadDetails}>
|
<div className={styles.downloadDetails}>
|
||||||
<div className={styles.downloadTitleWrapper}>
|
<div className={styles.downloadTitleWrapper}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.downloadTitle}
|
className={styles.downloadTitle}
|
||||||
onClick={() =>
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
navigate(`/game/${game.shop}/${game.objectID}`)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{game.title}
|
{game.title}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small className={styles.downloaderName}>
|
|
||||||
{downloaderName[game?.downloader]}
|
|
||||||
</small>
|
|
||||||
{getGameInfo(game)}
|
{getGameInfo(game)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { ShopDetails } from "@types";
|
|
||||||
|
|
||||||
import * as styles from "./game-details.css";
|
import * as styles from "./game-details.css";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { gameDetailsContext } from "./game-details.context";
|
||||||
|
|
||||||
export interface DescriptionHeaderProps {
|
export function DescriptionHeader() {
|
||||||
gameDetails: ShopDetails;
|
const { shopDetails } = useContext(gameDetailsContext);
|
||||||
}
|
|
||||||
|
|
||||||
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
if (!shopDetails) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.descriptionHeader}>
|
<div className={styles.descriptionHeader}>
|
||||||
<section className={styles.descriptionHeaderInfo}>
|
<section className={styles.descriptionHeaderInfo}>
|
||||||
<p>
|
<p>
|
||||||
{t("release_date", {
|
{t("release_date", {
|
||||||
date: gameDetails?.release_date.date,
|
date: shopDetails?.release_date.date,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p>{t("publisher", { publisher: gameDetails.publishers[0] })}</p>
|
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -67,6 +67,7 @@ export const mediaPreviewButton = recipe({
|
||||||
transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
|
transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
overflow: "hidden",
|
||||||
":hover": {
|
":hover": {
|
||||||
opacity: "0.8",
|
opacity: "0.8",
|
||||||
},
|
},
|
||||||
|
@ -84,7 +85,6 @@ export const mediaPreview = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flex: "1",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gallerySliderButton = recipe({
|
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 { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import type { ShopDetails } from "@types";
|
|
||||||
|
|
||||||
import * as styles from "./gallery-slider.css";
|
import * as styles from "./gallery-slider.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { gameDetailsContext } from "./game-details.context";
|
||||||
|
|
||||||
export interface GallerySliderProps {
|
export function GallerySlider() {
|
||||||
gameDetails: ShopDetails;
|
const { shopDetails } = useContext(gameDetailsContext);
|
||||||
}
|
|
||||||
|
|
||||||
export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const mediaContainerRef = useRef<HTMLDivElement>(null);
|
const mediaContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const hasScreenshots = gameDetails && gameDetails.screenshots.length;
|
const hasScreenshots = shopDetails && shopDetails.screenshots.length;
|
||||||
const hasMovies = gameDetails && gameDetails.movies?.length;
|
const hasMovies = shopDetails && shopDetails.movies?.length;
|
||||||
|
|
||||||
const [mediaCount] = useState<number>(() => {
|
const mediaCount = useMemo(() => {
|
||||||
if (gameDetails.screenshots && gameDetails.movies) {
|
if (!shopDetails) return 0;
|
||||||
return gameDetails.screenshots.length + gameDetails.movies.length;
|
|
||||||
} else if (gameDetails.movies) {
|
if (shopDetails.screenshots && shopDetails.movies) {
|
||||||
return gameDetails.movies.length;
|
return shopDetails.screenshots.length + shopDetails.movies.length;
|
||||||
} else if (gameDetails.screenshots) {
|
} else if (shopDetails.movies) {
|
||||||
return gameDetails.screenshots.length;
|
return shopDetails.movies.length;
|
||||||
|
} else if (shopDetails.screenshots) {
|
||||||
|
return shopDetails.screenshots.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
}, [shopDetails]);
|
||||||
|
|
||||||
const [mediaIndex, setMediaIndex] = useState<number>(0);
|
const [mediaIndex, setMediaIndex] = useState(0);
|
||||||
const [showArrows, setShowArrows] = useState(false);
|
const [showArrows, setShowArrows] = useState(false);
|
||||||
|
|
||||||
const showNextImage = () => {
|
const showNextImage = () => {
|
||||||
|
@ -52,7 +51,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMediaIndex(0);
|
setMediaIndex(0);
|
||||||
}, [gameDetails]);
|
}, [shopDetails]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasMovies && mediaContainerRef.current) {
|
if (hasMovies && mediaContainerRef.current) {
|
||||||
|
@ -74,17 +73,17 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||||
const scrollLeft = mediaIndex * itemWidth;
|
const scrollLeft = mediaIndex * itemWidth;
|
||||||
container.scrollLeft = scrollLeft;
|
container.scrollLeft = scrollLeft;
|
||||||
}
|
}
|
||||||
}, [gameDetails, mediaIndex, mediaCount]);
|
}, [shopDetails, mediaIndex, mediaCount]);
|
||||||
|
|
||||||
const previews = useMemo(() => {
|
const previews = useMemo(() => {
|
||||||
const screenshotPreviews =
|
const screenshotPreviews =
|
||||||
gameDetails?.screenshots.map(({ id, path_thumbnail }) => ({
|
shopDetails?.screenshots.map(({ id, path_thumbnail }) => ({
|
||||||
id,
|
id,
|
||||||
thumbnail: path_thumbnail,
|
thumbnail: path_thumbnail,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
if (gameDetails?.movies) {
|
if (shopDetails?.movies) {
|
||||||
const moviePreviews = gameDetails.movies.map(({ id, thumbnail }) => ({
|
const moviePreviews = shopDetails.movies.map(({ id, thumbnail }) => ({
|
||||||
id,
|
id,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
}));
|
}));
|
||||||
|
@ -93,7 +92,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return screenshotPreviews;
|
return screenshotPreviews;
|
||||||
}, [gameDetails]);
|
}, [shopDetails]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -105,8 +104,8 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||||
className={styles.gallerySliderAnimationContainer}
|
className={styles.gallerySliderAnimationContainer}
|
||||||
ref={mediaContainerRef}
|
ref={mediaContainerRef}
|
||||||
>
|
>
|
||||||
{gameDetails.movies &&
|
{shopDetails.movies &&
|
||||||
gameDetails.movies.map((video) => (
|
shopDetails.movies.map((video) => (
|
||||||
<video
|
<video
|
||||||
key={video.id}
|
key={video.id}
|
||||||
controls
|
controls
|
||||||
|
@ -122,7 +121,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{hasScreenshots &&
|
{hasScreenshots &&
|
||||||
gameDetails.screenshots.map((image, i) => (
|
shopDetails.screenshots.map((image, i) => (
|
||||||
<img
|
<img
|
||||||
key={image.id}
|
key={image.id}
|
||||||
className={styles.gallerySliderMedia}
|
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";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const slideIn = keyframes({
|
export const slideIn = keyframes({
|
||||||
"0%": { transform: `translateY(${40 + 16}px)` },
|
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` },
|
||||||
"100%": { transform: "translateY(0)" },
|
"100%": { transform: "translateY(0)" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -174,5 +174,5 @@ globalStyle(`${description} img`, {
|
||||||
});
|
});
|
||||||
|
|
||||||
globalStyle(`${description} a`, {
|
globalStyle(`${description} a`, {
|
||||||
color: vars.color.bodyText,
|
color: vars.color.body,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,24 +1,11 @@
|
||||||
import Color from "color";
|
import { useEffect, useState } from "react";
|
||||||
import { average } from "color.js";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
|
import { average } from "color.js";
|
||||||
|
|
||||||
import {
|
import { Steam250Game } from "@types";
|
||||||
Steam250Game,
|
|
||||||
type Game,
|
|
||||||
type GameRepack,
|
|
||||||
type GameShop,
|
|
||||||
type ShopDetails,
|
|
||||||
} from "@types";
|
|
||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
||||||
import {
|
|
||||||
buildGameDetailsPath,
|
|
||||||
getSteamLanguage,
|
|
||||||
steamUrlBuilder,
|
|
||||||
} from "@renderer/helpers";
|
|
||||||
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
|
||||||
|
|
||||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
|
|
||||||
|
@ -29,153 +16,34 @@ import { DescriptionHeader } from "./description-header";
|
||||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||||
import * as styles from "./game-details.css";
|
import * as styles from "./game-details.css";
|
||||||
import { HeroPanel } from "./hero";
|
import { HeroPanel } from "./hero";
|
||||||
import { RepacksModal } from "./repacks-modal";
|
|
||||||
|
|
||||||
import { vars } from "../../theme.css";
|
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 { GallerySlider } from "./gallery-slider";
|
||||||
import { Sidebar } from "./sidebar/sidebar";
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
|
import {
|
||||||
|
GameDetailsContextConsumer,
|
||||||
|
GameDetailsContextProvider,
|
||||||
|
} from "./game-details.context";
|
||||||
|
|
||||||
export function GameDetails() {
|
export function GameDetails() {
|
||||||
const { objectID, shop } = useParams();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
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 { objectID } = useParams();
|
||||||
const [isGamePlaying, setIsGamePlaying] = useState(false);
|
|
||||||
const [showInstructionsModal, setShowInstructionsModal] = useState<
|
|
||||||
null | "onlinefix" | "DODI"
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const fromRandomizer = searchParams.get("fromRandomizer");
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getGame();
|
setRandomGame(null);
|
||||||
}, [getGame, gameDownloading?.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setGameDetails(null);
|
|
||||||
setGame(null);
|
|
||||||
setIsLoading(true);
|
|
||||||
setIsGamePlaying(false);
|
|
||||||
dispatch(setHeaderTitle(title));
|
|
||||||
|
|
||||||
window.electron.getRandomGame().then((randomGame) => {
|
window.electron.getRandomGame().then((randomGame) => {
|
||||||
setRandomGame(randomGame);
|
setRandomGame(randomGame);
|
||||||
});
|
});
|
||||||
|
}, [objectID]);
|
||||||
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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRandomizerClick = () => {
|
const handleRandomizerClick = () => {
|
||||||
if (randomGame) {
|
if (randomGame) {
|
||||||
|
@ -189,32 +57,33 @@ export function GameDetails() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<GameDetailsContextProvider>
|
||||||
<RepacksModal
|
<GameDetailsContextConsumer>
|
||||||
visible={showRepacksModal}
|
{({ game, shopDetails, isLoading, setGameColor }) => {
|
||||||
repacks={repacks}
|
const handleHeroLoad = async () => {
|
||||||
startDownload={handleStartDownload}
|
const output = await average(
|
||||||
onClose={() => setShowRepacksModal(false)}
|
steamUrlBuilder.libraryHero(objectID!),
|
||||||
/>
|
{
|
||||||
|
amount: 1,
|
||||||
|
format: "hex",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
<OnlineFixInstallationGuide
|
setGameColor(output as string);
|
||||||
visible={showInstructionsModal === "onlinefix"}
|
};
|
||||||
onClose={() => setShowInstructionsModal(null)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DODIInstallationGuide
|
|
||||||
windowColor={color.light}
|
|
||||||
visible={showInstructionsModal === "DODI"}
|
|
||||||
onClose={() => setShowInstructionsModal(null)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme
|
||||||
|
baseColor={vars.color.background}
|
||||||
|
highlightColor="#444"
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<GameDetailsSkeleton />
|
<GameDetailsSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
<div className={styles.hero}>
|
<div className={styles.hero}>
|
||||||
<img
|
<img
|
||||||
src={heroImage}
|
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||||
className={styles.heroImage}
|
className={styles.heroImage}
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
|
@ -230,35 +99,23 @@ export function GameDetails() {
|
||||||
</div>
|
</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.descriptionContainer}>
|
||||||
<div className={styles.descriptionContent}>
|
<div className={styles.descriptionContent}>
|
||||||
{gameDetails && <DescriptionHeader gameDetails={gameDetails} />}
|
<DescriptionHeader />
|
||||||
{gameDetails && <GallerySlider gameDetails={gameDetails} />}
|
<GallerySlider />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: gameDetails?.about_the_game ?? t("no_shop_details"),
|
__html:
|
||||||
|
shopDetails?.about_the_game ?? t("no_shop_details"),
|
||||||
}}
|
}}
|
||||||
className={styles.description}
|
className={styles.description}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sidebar
|
<Sidebar />
|
||||||
objectID={objectID!}
|
|
||||||
title={title}
|
|
||||||
gameDetails={gameDetails}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
@ -273,7 +130,12 @@ export function GameDetails() {
|
||||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||||
<Lottie
|
<Lottie
|
||||||
animationData={starsAnimation}
|
animationData={starsAnimation}
|
||||||
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
style={{
|
||||||
|
width: 70,
|
||||||
|
position: "absolute",
|
||||||
|
top: -28,
|
||||||
|
left: -27,
|
||||||
|
}}
|
||||||
loop
|
loop
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -282,4 +144,8 @@ export function GameDetails() {
|
||||||
)}
|
)}
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
);
|
);
|
||||||
|
}}
|
||||||
|
</GameDetailsContextConsumer>
|
||||||
|
</GameDetailsContextProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,20 @@
|
||||||
import { GameStatus, GameStatusHelper } from "@shared";
|
|
||||||
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
|
||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import type { Game, GameRepack } from "@types";
|
import { useContext, useState } from "react";
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./hero-panel-actions.css";
|
import * as styles from "./hero-panel-actions.css";
|
||||||
|
import { gameDetailsContext } from "../game-details.context";
|
||||||
|
import { Downloader } from "@shared";
|
||||||
|
|
||||||
export interface HeroPanelActionsProps {
|
export function HeroPanelActions() {
|
||||||
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) {
|
|
||||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
resumeDownload,
|
resumeDownload,
|
||||||
|
@ -43,12 +24,25 @@ export function HeroPanelActions({
|
||||||
isGameDeleting,
|
isGameDeleting,
|
||||||
} = useDownload();
|
} = useDownload();
|
||||||
|
|
||||||
|
const {
|
||||||
|
game,
|
||||||
|
repacks,
|
||||||
|
isGameRunning,
|
||||||
|
objectID,
|
||||||
|
gameTitle,
|
||||||
|
openRepacksModal,
|
||||||
|
updateGame,
|
||||||
|
} = useContext(gameDetailsContext);
|
||||||
|
|
||||||
|
const userPreferences = useAppSelector(
|
||||||
|
(state) => state.userPreferences.value
|
||||||
|
);
|
||||||
|
|
||||||
const { updateLibrary } = useLibrary();
|
const { updateLibrary } = useLibrary();
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const getDownloadsPath = async () => {
|
const getDownloadsPath = async () => {
|
||||||
const userPreferences = await window.electron.getUserPreferences();
|
|
||||||
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||||
return window.electron.getDefaultDownloadsPath();
|
return window.electron.getDefaultDownloadsPath();
|
||||||
};
|
};
|
||||||
|
@ -86,15 +80,15 @@ export function HeroPanelActions({
|
||||||
const gameExecutablePath = await selectGameExecutable();
|
const gameExecutablePath = await selectGameExecutable();
|
||||||
|
|
||||||
await window.electron.addGameToLibrary(
|
await window.electron.addGameToLibrary(
|
||||||
objectID,
|
objectID!,
|
||||||
title,
|
gameTitle,
|
||||||
"steam",
|
"steam",
|
||||||
gameExecutablePath
|
gameExecutablePath
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
getGame();
|
updateGame();
|
||||||
} finally {
|
} finally {
|
||||||
setToggleLibraryGameDisabled(false);
|
setToggleLibraryGameDisabled(false);
|
||||||
}
|
}
|
||||||
|
@ -103,7 +97,7 @@ export function HeroPanelActions({
|
||||||
const openGameInstaller = () => {
|
const openGameInstaller = () => {
|
||||||
if (game) {
|
if (game) {
|
||||||
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
|
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
|
||||||
if (!isBinaryInPath) openBinaryNotFoundModal();
|
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -145,18 +139,18 @@ export function HeroPanelActions({
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (game && isGameDownloading) {
|
if (game?.status === "active" && game?.progress !== 1) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => pauseDownload(game.id)}
|
onClick={() => pauseDownload(game.id).then(updateGame)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
{t("pause")}
|
{t("pause")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => cancelDownload(game.id)}
|
onClick={() => cancelDownload(game.id).then(updateGame)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
|
@ -166,18 +160,22 @@ export function HeroPanelActions({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game?.status === GameStatus.Paused) {
|
if (game?.status === "paused") {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => resumeDownload(game.id)}
|
onClick={() => resumeDownload(game.id).then(updateGame)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
|
disabled={
|
||||||
|
game.downloader === Downloader.RealDebrid &&
|
||||||
|
!userPreferences?.realDebridApiToken
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t("resume")}
|
{t("resume")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => cancelDownload(game.id).then(getGame)}
|
onClick={() => cancelDownload(game.id).then(updateGame)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
|
@ -187,49 +185,7 @@ export function HeroPanelActions({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (game?.status === "removed") {
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
@ -240,8 +196,9 @@ export function HeroPanelActions({
|
||||||
>
|
>
|
||||||
{t("open_download_options")}
|
{t("open_download_options")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => removeGameFromLibrary(game.id).then(getGame)}
|
onClick={() => removeGameFromLibrary(game.id).then(updateGame)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
|
@ -252,7 +209,7 @@ export function HeroPanelActions({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repacks.length) {
|
if (repacks.length && !game) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toggleGameOnLibraryButton}
|
{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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { Game } from "@types";
|
|
||||||
import { useDate } from "@renderer/hooks";
|
import { useDate } from "@renderer/hooks";
|
||||||
|
import { gameDetailsContext } from "../game-details.context";
|
||||||
|
|
||||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
export interface HeroPanelPlaytimeProps {
|
export function HeroPanelPlaytime() {
|
||||||
game: Game;
|
|
||||||
isGamePlaying: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeroPanelPlaytime({
|
|
||||||
game,
|
|
||||||
isGamePlaying,
|
|
||||||
}: HeroPanelPlaytimeProps) {
|
|
||||||
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
||||||
|
|
||||||
|
const { game, isGameRunning } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { i18n, t } = useTranslation("game_details");
|
const { i18n, t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { formatDistance } = useDate();
|
const { formatDistance } = useDate();
|
||||||
|
@ -52,8 +46,8 @@ export function HeroPanelPlaytime({
|
||||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!game.lastTimePlayed) {
|
if (!game?.lastTimePlayed) {
|
||||||
return <p>{t("not_played_yet", { title: game.title })}</p>;
|
return <p>{t("not_played_yet", { title: game?.title })}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -64,7 +58,7 @@ export function HeroPanelPlaytime({
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isGamePlaying ? (
|
{isGameRunning ? (
|
||||||
<p>{t("playing_now")}</p>
|
<p>{t("playing_now")}</p>
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
export const panel = style({
|
export const panel = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -11,6 +12,8 @@ export const panel = style({
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
borderBottom: `solid 1px ${vars.color.border}`,
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
|
@ -29,3 +32,27 @@ export const downloadDetailsRow = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-end",
|
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 { format } from "date-fns";
|
||||||
import { useMemo, useState } from "react";
|
import { useContext, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Color from "color";
|
||||||
|
|
||||||
import { useDownload } from "@renderer/hooks";
|
import { useDownload } from "@renderer/hooks";
|
||||||
import type { Game, GameRepack } from "@types";
|
|
||||||
|
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
import { HeroPanelActions } from "./hero-panel-actions";
|
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 * as styles from "./hero-panel.css";
|
||||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||||
|
import { gameDetailsContext } from "../game-details.context";
|
||||||
|
|
||||||
export interface HeroPanelProps {
|
export function HeroPanel() {
|
||||||
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) {
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const {
|
const { progress, eta, lastPacket, isGameDeleting } = useDownload();
|
||||||
game: gameDownloading,
|
|
||||||
progress,
|
|
||||||
eta,
|
|
||||||
numPeers,
|
|
||||||
numSeeds,
|
|
||||||
isGameDeleting,
|
|
||||||
} = useDownload();
|
|
||||||
|
|
||||||
const isGameDownloading =
|
|
||||||
gameDownloading?.id === game?.id &&
|
|
||||||
GameStatusHelper.isDownloading(game?.status ?? null);
|
|
||||||
|
|
||||||
const finalDownloadSize = useMemo(() => {
|
const finalDownloadSize = useMemo(() => {
|
||||||
if (!game) return "N/A";
|
if (!game) return "N/A";
|
||||||
if (game.fileSize) return formatBytes(game.fileSize);
|
if (game.fileSize) return formatBytes(game.fileSize);
|
||||||
|
|
||||||
if (gameDownloading?.fileSize && isGameDownloading)
|
if (lastPacket?.game.fileSize && game?.status === "active")
|
||||||
return formatBytes(gameDownloading.fileSize);
|
return formatBytes(lastPacket?.game.fileSize);
|
||||||
|
|
||||||
return game.repack?.fileSize ?? "N/A";
|
return game.repack?.fileSize ?? "N/A";
|
||||||
}, [game, isGameDownloading, gameDownloading]);
|
}, [game, lastPacket?.game]);
|
||||||
|
|
||||||
|
const isGameDownloading =
|
||||||
|
game?.status === "active" && lastPacket?.game.id === game?.id;
|
||||||
|
|
||||||
const getInfo = () => {
|
const getInfo = () => {
|
||||||
if (isGameDeleting(game?.id ?? -1)) {
|
if (isGameDeleting(game?.id ?? -1)) return <p>{t("deleting")}</p>;
|
||||||
return <p>{t("deleting")}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGameDownloading && gameDownloading?.status) {
|
if (game?.progress === 1) return <HeroPanelPlaytime />;
|
||||||
|
|
||||||
|
if (game?.status === "active") {
|
||||||
|
if (lastPacket?.isDownloadingMetadata && isGameDownloading) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className={styles.downloadDetailsRow}>
|
<p>{progress}</p>
|
||||||
{progress}
|
<p>{t("downloading_metadata")}</p>
|
||||||
{eta && <small>{t("eta", { 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}
|
|
||||||
<small>
|
|
||||||
{game?.downloader === Downloader.Torrent &&
|
|
||||||
`${numPeers} peers / ${numSeeds} seeds`}
|
|
||||||
</small>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game?.status === GameStatus.Paused) {
|
const sizeDownloaded = formatBytes(
|
||||||
|
lastPacket?.game?.bytesDownloaded ?? game?.bytesDownloaded
|
||||||
|
);
|
||||||
|
|
||||||
|
const showPeers =
|
||||||
|
game?.downloader === Downloader.Torrent &&
|
||||||
|
lastPacket?.numPeers !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p className={styles.downloadDetailsRow}>
|
||||||
{t("paused_progress", {
|
{isGameDownloading
|
||||||
progress: formatDownloadProgress(game.progress),
|
? progress
|
||||||
})}
|
: formatDownloadProgress(game?.progress)}
|
||||||
|
|
||||||
|
<small>{eta ? t("eta", { eta }) : t("calculating_eta")}</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className={styles.downloadDetailsRow}>
|
||||||
|
<span>
|
||||||
|
{sizeDownloaded} / {finalDownloadSize}
|
||||||
|
</span>
|
||||||
|
{showPeers && (
|
||||||
|
<small>
|
||||||
|
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game?.status === "paused") {
|
||||||
|
const formattedProgress = formatDownloadProgress(game.progress);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className={styles.downloadDetailsRow}>
|
||||||
|
{formattedProgress} <small>{t("paused")}</small>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
{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;
|
const [latestRepack] = repacks;
|
||||||
|
|
||||||
if (latestRepack) {
|
if (latestRepack) {
|
||||||
|
@ -129,28 +112,33 @@ export function HeroPanel({
|
||||||
return <p>{t("no_downloads")}</p>;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<BinaryNotFoundModal
|
<div style={{ backgroundColor }} className={styles.panel}>
|
||||||
visible={showBinaryNotFoundModal}
|
|
||||||
onClose={() => setShowBinaryNotFoundModal(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ backgroundColor: color }} className={styles.panel}>
|
|
||||||
<div className={styles.content}>{getInfo()}</div>
|
<div className={styles.content}>{getInfo()}</div>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<HeroPanelActions
|
<HeroPanelActions />
|
||||||
game={game}
|
|
||||||
repacks={repacks}
|
|
||||||
objectID={objectID}
|
|
||||||
title={title}
|
|
||||||
getGame={getGame}
|
|
||||||
openRepacksModal={openRepacksModal}
|
|
||||||
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
|
|
||||||
isGamePlaying={isGamePlaying}
|
|
||||||
isGameDownloading={isGameDownloading}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showProgressBar && (
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={
|
||||||
|
isGameDownloading ? lastPacket?.game.progress : game?.progress
|
||||||
|
}
|
||||||
|
className={styles.progressBar({
|
||||||
|
disabled: game?.status === "paused",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue