mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-27 12:51:03 +00:00
merge branch 'main' of https://github.com/KelvinDiasMoreira/hydra into feature/delete-all-dowload-sources
This commit is contained in:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: main
|
branches: main
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.1.5",
|
"version": "3.3.0",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@fontsource/noto-sans": "^5.1.0",
|
"@fontsource/noto-sans": "^5.1.0",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"@sentry/vite-plugin": "^2.22.7",
|
"@sentry/vite-plugin": "^2.22.7",
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"axios-cookiejar-support": "^5.0.5",
|
||||||
"better-sqlite3": "^11.7.0",
|
"better-sqlite3": "^11.7.0",
|
||||||
"classic-level": "^2.0.0",
|
"classic-level": "^2.0.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -59,6 +61,7 @@
|
|||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"kill-port": "^2.0.1",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"parse-torrent": "^11.0.17",
|
"parse-torrent": "^11.0.17",
|
||||||
@@ -69,9 +72,11 @@
|
|||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
"react-redux": "^9.1.1",
|
"react-redux": "^9.1.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
|
"react-tooltip": "^5.28.0",
|
||||||
"sound-play": "^1.1.0",
|
"sound-play": "^1.1.0",
|
||||||
"sudo-prompt": "^9.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
|
"tough-cookie": "^5.1.1",
|
||||||
"user-agents": "^1.1.387",
|
"user-agents": "^1.1.387",
|
||||||
"yaml": "^2.6.1",
|
"yaml": "^2.6.1",
|
||||||
"yup": "^1.5.0",
|
"yup": "^1.5.0",
|
||||||
|
|||||||
@@ -159,6 +159,8 @@ def action():
|
|||||||
downloader = downloads.get(game_id)
|
downloader = downloads.get(game_id)
|
||||||
if downloader:
|
if downloader:
|
||||||
downloader.pause_download()
|
downloader.pause_download()
|
||||||
|
|
||||||
|
if downloading_game_id == game_id:
|
||||||
downloading_game_id = -1
|
downloading_game_id = -1
|
||||||
elif action == 'cancel':
|
elif action == 'cancel':
|
||||||
downloader = downloads.get(game_id)
|
downloader = downloads.get(game_id)
|
||||||
|
|||||||
@@ -107,7 +107,10 @@ const copyAria2Macos = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copyAria2 = () => {
|
const copyAria2 = () => {
|
||||||
if (fs.existsSync("aria2")) {
|
const aria2Path =
|
||||||
|
process.platform === "win32" ? "aria2/aria2c.exe" : "aria2/aria2c";
|
||||||
|
|
||||||
|
if (fs.existsSync(aria2Path)) {
|
||||||
console.log("Aria2 already exists, skipping download...");
|
console.log("Aria2 already exists, skipping download...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -7,18 +7,18 @@
|
|||||||
"featured": "مميز",
|
"featured": "مميز",
|
||||||
"surprise_me": "مفاجئني",
|
"surprise_me": "مفاجئني",
|
||||||
"no_results": "لم يتم العثور على نتائج",
|
"no_results": "لم يتم العثور على نتائج",
|
||||||
"start_typing": "ابدأ الكتابة للبحث...",
|
"start_typing": "ابدأ بالكتابة للبحث...",
|
||||||
"hot": "الأكثر شيوعًا الآن",
|
"hot": "الأكثر شهرة الآن",
|
||||||
"weekly": "📅 أفضل ألعاب الأسبوع",
|
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||||
"achievements": "🏆 ألعاب للتغلب عليها"
|
"achievements": "🏆 ألعاب يجب إكمالها"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "الكـتالوج",
|
"catalogue": "الفهرس",
|
||||||
"downloads": "التنزيلات",
|
"downloads": "التنزيلات",
|
||||||
"settings": "الإعدادات",
|
"settings": "الإعدادات",
|
||||||
"my_library": "مكتبتي",
|
"my_library": "مكتبتي",
|
||||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
"downloading_metadata": "{{title}} (جاري تنزيل البيانات الوصفية...)",
|
||||||
"paused": "{{title}} (معلّق)",
|
"paused": "{{title}} (معلق)",
|
||||||
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||||
"filter": "تصفية المكتبة",
|
"filter": "تصفية المكتبة",
|
||||||
"home": "الرئيسية",
|
"home": "الرئيسية",
|
||||||
@@ -26,12 +26,13 @@
|
|||||||
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
||||||
"sign_in": "تسجيل الدخول",
|
"sign_in": "تسجيل الدخول",
|
||||||
"friends": "الأصدقاء",
|
"friends": "الأصدقاء",
|
||||||
"need_help": "تحتاج مساعدة؟"
|
"need_help": "تحتاج مساعدة؟",
|
||||||
|
"favorites": "المفضلة"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "ابحث عن الألعاب",
|
"search": "بحث الألعاب",
|
||||||
"home": "الرئيسية",
|
"home": "الرئيسية",
|
||||||
"catalogue": "الكـتالوج",
|
"catalogue": "الفهرس",
|
||||||
"downloads": "التنزيلات",
|
"downloads": "التنزيلات",
|
||||||
"search_results": "نتائج البحث",
|
"search_results": "نتائج البحث",
|
||||||
"settings": "الإعدادات",
|
"settings": "الإعدادات",
|
||||||
@@ -40,16 +41,16 @@
|
|||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية لـ {{title}}...",
|
"downloading_metadata": "جاري تنزيل بيانات {{title}} الوصفية...",
|
||||||
"downloading": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
"downloading": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||||
"calculating_eta": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - جاري حساب الوقت المتبقي...",
|
"calculating_eta": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - جاري حساب الوقت المتبقي...",
|
||||||
"checking_files": "جارٍ فحص ملفات {{title}}... ({{percentage}} اكتمال)"
|
"checking_files": "جاري فحص ملفات {{title}}... ({{percentage}} مكتمل)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"search": "تصفية...",
|
"search": "تصفية...",
|
||||||
"developers": "المطورون",
|
"developers": "المطورون",
|
||||||
"genres": "الأنواع",
|
"genres": "الأنواع",
|
||||||
"tags": "العلامات",
|
"tags": "الوسوم",
|
||||||
"publishers": "الناشرون",
|
"publishers": "الناشرون",
|
||||||
"download_sources": "مصادر التنزيل",
|
"download_sources": "مصادر التنزيل",
|
||||||
"result_count": "{{resultCount}} نتيجة",
|
"result_count": "{{resultCount}} نتيجة",
|
||||||
@@ -68,34 +69,34 @@
|
|||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"remove": "إزالة",
|
"remove": "إزالة",
|
||||||
"space_left_on_disk": "{{space}} متبقي على القرص",
|
"space_left_on_disk": "{{space}} متبقي على القرص",
|
||||||
"eta": "الانتهاء {{eta}}",
|
"eta": "الانتهاء المتوقع {{eta}}",
|
||||||
"calculating_eta": "جارٍ حساب الوقت المتبقي...",
|
"calculating_eta": "جاري حساب الوقت المتبقي...",
|
||||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||||
"filter": "تصفية الحزم المعاد تعبئتها",
|
"filter": "تصفية الإصدارات المعادة",
|
||||||
"requirements": "متطلبات النظام",
|
"requirements": "متطلبات النظام",
|
||||||
"minimum": "الحد الأدنى",
|
"minimum": "الحد الأدنى",
|
||||||
"recommended": "مُوصى به",
|
"recommended": "مستحسن",
|
||||||
"paused": "معلّق",
|
"paused": "معلق",
|
||||||
"release_date": "تاريخ الإصدار {{date}}",
|
"release_date": "تاريخ الإصدار {{date}}",
|
||||||
"publisher": "نشر بواسطة {{publisher}}",
|
"publisher": "نشر بواسطة {{publisher}}",
|
||||||
"hours": "ساعات",
|
"hours": "ساعات",
|
||||||
"minutes": "دقائق",
|
"minutes": "دقائق",
|
||||||
"amount_hours": "{{amount}} ساعات",
|
"amount_hours": "{{amount}} ساعة",
|
||||||
"amount_minutes": "{{amount}} دقائق",
|
"amount_minutes": "{{amount}} دقيقة",
|
||||||
"accuracy": "دقة {{accuracy}}%",
|
"accuracy": "دقة {{accuracy}}%",
|
||||||
"add_to_library": "إضافة إلى المكتبة",
|
"add_to_library": "إضافة إلى المكتبة",
|
||||||
"remove_from_library": "إزالة من المكتبة",
|
"remove_from_library": "إزالة من المكتبة",
|
||||||
"no_downloads": "لا توجد تنزيلات متاحة",
|
"no_downloads": "لا توجد تنزيلات متاحة",
|
||||||
"play_time": "لعب لمدة {{amount}}",
|
"play_time": "وقت اللعب {{amount}}",
|
||||||
"last_time_played": "آخر تشغيل {{period}}",
|
"last_time_played": "آخر مرة لعب {{period}}",
|
||||||
"not_played_yet": "لم تلعب {{title}} بعد",
|
"not_played_yet": "لم تلعب {{title}} بعد",
|
||||||
"next_suggestion": "الاقتراح التالي",
|
"next_suggestion": "الاقتراح التالي",
|
||||||
"play": "تشغيل",
|
"play": "تشغيل",
|
||||||
"deleting": "جارٍ حذف المثبت...",
|
"deleting": "جاري حذف المثبت...",
|
||||||
"close": "إغلاق",
|
"close": "إغلاق",
|
||||||
"playing_now": "يتم التشغيل الآن",
|
"playing_now": "جاري التشغيل الآن",
|
||||||
"change": "تغيير",
|
"change": "تغيير",
|
||||||
"repacks_modal_description": "اختر الحزمة المعاد تعبئتها التي تريد تنزيلها",
|
"repacks_modal_description": "اختر الإصدار المعاد الذي تريد تنزيله",
|
||||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
||||||
"download_now": "تنزيل الآن",
|
"download_now": "تنزيل الآن",
|
||||||
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
||||||
@@ -110,12 +111,12 @@
|
|||||||
"select_executable": "تحديد",
|
"select_executable": "تحديد",
|
||||||
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
||||||
"open_folder": "فتح المجلد",
|
"open_folder": "فتح المجلد",
|
||||||
"open_download_location": "عرض الملفات المحملة",
|
"open_download_location": "عرض الملفات المنزلة",
|
||||||
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
||||||
"clear": "مسح",
|
"clear": "مسح",
|
||||||
"remove_files": "إزالة الملفات",
|
"remove_files": "إزالة الملفات",
|
||||||
"remove_from_library_title": "هل أنت متأكد؟",
|
"remove_from_library_title": "هل أنت متأكد؟",
|
||||||
"remove_from_library_description": "سيؤدي هذا إلى إزالة {{game}} من مكتبتك",
|
"remove_from_library_description": "سيتم إزالة {{game}} من مكتبتك",
|
||||||
"options": "خيارات",
|
"options": "خيارات",
|
||||||
"executable_section_title": "ملف التشغيل",
|
"executable_section_title": "ملف التشغيل",
|
||||||
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||||
@@ -123,35 +124,35 @@
|
|||||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||||
"danger_zone_section_title": "منطقة الخطر",
|
"danger_zone_section_title": "منطقة الخطر",
|
||||||
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||||
"download_in_progress": "تنزيل قيد التقدم",
|
"download_in_progress": "جاري التنزيل",
|
||||||
"download_paused": "التنزيل معلق",
|
"download_paused": "التنزيل معلق",
|
||||||
"last_downloaded_option": "خيار التنزيل الأخير",
|
"last_downloaded_option": "خيار التنزيل الأخير",
|
||||||
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||||
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
||||||
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
||||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يناسب جميع الأعمار. هل تريد المتابعة؟",
|
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يكون مناسبًا لجميع الأعمار. هل تريد المتابعة؟",
|
||||||
"allow_nsfw_content": "متابعة",
|
"allow_nsfw_content": "متابعة",
|
||||||
"refuse_nsfw_content": "رجوع",
|
"refuse_nsfw_content": "رجوع",
|
||||||
"stats": "الإحصائيات",
|
"stats": "الإحصائيات",
|
||||||
"download_count": "مرات التنزيل",
|
"download_count": "التنزيلات",
|
||||||
"player_count": "اللاعبون النشطون",
|
"player_count": "اللاعبون النشطون",
|
||||||
"download_error": "خيار التنزيل هذا غير متاح",
|
"download_error": "خيار التنزيل هذا غير متاح",
|
||||||
"download": "تنزيل",
|
"download": "تنزيل",
|
||||||
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
||||||
"warning": "تحذير:",
|
"warning": "تحذير:",
|
||||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يبقى Hydra مفتوحًا حتى اكتماله. إذا أغلق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يظل Hydra مفتوحًا حتى اكتماله. إذا تم إغلاق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||||
"achievements": "الإنجازات",
|
"achievements": "الإنجازات",
|
||||||
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "حفظ سحابي",
|
"cloud_save": "حفظ سحابي",
|
||||||
"cloud_save_description": "احفظ تقدمك على السحابة واستمر في اللعب من أي جهاز",
|
"cloud_save_description": "احفظ تقدمك في السحابة واستمر في اللعب من أي جهاز",
|
||||||
"backups": "النسخ الاحتياطية",
|
"backups": "النسخ الاحتياطية",
|
||||||
"install_backup": "تثبيت",
|
"install_backup": "تثبيت",
|
||||||
"delete_backup": "حذف",
|
"delete_backup": "حذف",
|
||||||
"create_backup": "نسخة احتياطية جديدة",
|
"create_backup": "نسخة احتياطية جديدة",
|
||||||
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
||||||
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
||||||
"restoring_backup": "جارٍ استعادة النسخة الاحتياطية ({{progress}} اكتمال)...",
|
"restoring_backup": "جاري استعادة النسخة الاحتياطية ({{progress}} مكتمل)...",
|
||||||
"uploading_backup": "جارٍ رفع النسخة الاحتياطية...",
|
"uploading_backup": "جاري رفع النسخة الاحتياطية...",
|
||||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
||||||
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
||||||
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||||
@@ -164,61 +165,67 @@
|
|||||||
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||||
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||||
"manage_files": "إدارة الملفات",
|
"manage_files": "إدارة الملفات",
|
||||||
"loading_save_preview": "جارٍ البحث عن حفظات الألعاب...",
|
"loading_save_preview": "جاري البحث عن حفظات اللعبة...",
|
||||||
"wine_prefix": "بادئة Wine",
|
"wine_prefix": "بادئة Wine",
|
||||||
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||||
"launch_options": "خيارات التشغيل",
|
"launch_options": "خيارات التشغيل",
|
||||||
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
||||||
"launch_options_placeholder": "لم يتم تحديد أي معاملات",
|
"launch_options_placeholder": "لا توجد معلمات محددة",
|
||||||
"no_download_option_info": "لا توجد معلومات متاحة",
|
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||||
"backup_deletion_failed": "فشل حذف النسخة الاحتياطية",
|
"backup_deletion_failed": "فشل في حذف النسخة الاحتياطية",
|
||||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى من النسخ الاحتياطية لهذه اللعبة",
|
||||||
"achievements_not_sync": "تعرف على كيفية مزامنة إنجازاتك",
|
"achievements_not_sync": "شاهد كيفية مزامنة إنجازاتك",
|
||||||
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||||
"select_folder": "حدد المجلد",
|
"select_folder": "حدد المجلد",
|
||||||
"backup_from": "نسخة احتياطية من {{date}}",
|
"backup_from": "نسخة احتياطية من {{date}}",
|
||||||
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
||||||
"no_directory_selected": "لم يتم تحديد مجلد",
|
"no_directory_selected": "لم يتم تحديد مجلد",
|
||||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا لمعرفة المزيد.",
|
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا للمزيد من المعلومات.",
|
||||||
"reset_achievements": "إعادة تعيين الإنجازات",
|
"reset_achievements": "إعادة تعيين الإنجازات",
|
||||||
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
||||||
"reset_achievements_title": "هل أنت متأكد؟",
|
"reset_achievements_title": "هل أنت متأكد؟",
|
||||||
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
||||||
"reset_achievements_error": "فشل إعادة تعيين الإنجازات"
|
"reset_achievements_error": "فشل في إعادة تعيين الإنجازات",
|
||||||
|
"download_error_gofile_quota_exceeded": "لقد تجاوزت الحصة الشهرية لـ Gofile. يرجى الانتظار حتى إعادة تعيين الحصة.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "حساب Real-Debrid الخاص بك غير مصرح له بإجراء تنزيلات جديدة. يرجى مراجعة إعدادات الحساب والمحاولة مرة أخرى.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "هذا التنزيل غير متوفر على Real-Debrid وجلب حالة التنزيل من Real-Debrid غير متاح حاليًا.",
|
||||||
|
"download_error_not_cached_in_torbox": "هذا التنزيل غير متوفر على Torbox وجلب حالة التنزيل من Torbox غير متاح حاليًا.",
|
||||||
|
"game_removed_from_favorites": "تمت إزالة اللعبة من المفضلة",
|
||||||
|
"game_added_to_favorites": "تمت إضافة اللعبة إلى المفضلة"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "تفعيل Hydra",
|
"title": "تفعيل Hydra",
|
||||||
"installation_id": "معرف التثبيت:",
|
"installation_id": "معرف التثبيت:",
|
||||||
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا يجب أن يكون لديك هذا.",
|
"message": "إذا كنت لا تعرف أين تطلب هذا، فأنت لا يجب أن يكون لديك هذا.",
|
||||||
"activate": "تفعيل",
|
"activate": "تفعيل",
|
||||||
"loading": "جارٍ التحميل..."
|
"loading": "جاري التحميل..."
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "استئناف",
|
"resume": "استئناف",
|
||||||
"pause": "إيقاف مؤقت",
|
"pause": "إيقاف مؤقت",
|
||||||
"eta": "الانتهاء {{eta}}",
|
"eta": "الانتهاء المتوقع {{eta}}",
|
||||||
"paused": "معلّق",
|
"paused": "معلق",
|
||||||
"verifying": "جارٍ التحقق...",
|
"verifying": "جاري التحقق...",
|
||||||
"completed": "مكتمل",
|
"completed": "مكتمل",
|
||||||
"removed": "غير محمل",
|
"removed": "غير منزّل",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"filter": "تصفية الألعاب المحملة",
|
"filter": "تصفية الألعاب المنزلة",
|
||||||
"remove": "إزالة",
|
"remove": "إزالة",
|
||||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||||
"deleting": "جارٍ حذف المثبت...",
|
"deleting": "جاري حذف المثبت...",
|
||||||
"delete": "إزالة المثبت",
|
"delete": "حذف المثبت",
|
||||||
"delete_modal_title": "هل أنت متأكد؟",
|
"delete_modal_title": "هل أنت متأكد؟",
|
||||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
||||||
"install": "تثبيت",
|
"install": "تثبيت",
|
||||||
"download_in_progress": "قيد التقدم",
|
"download_in_progress": "قيد التقدم",
|
||||||
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||||
"downloads_completed": "مكتمل",
|
"downloads_completed": "مكتملة",
|
||||||
"queued": "في قائمة الانتظار",
|
"queued": "في قائمة الانتظار",
|
||||||
"no_downloads_title": "فارغ جدًا",
|
"no_downloads_title": "لا شيء هنا",
|
||||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
||||||
"checking_files": "جارٍ فحص الملفات...",
|
"checking_files": "جاري فحص الملفات...",
|
||||||
"seeding": "التوزيع",
|
"seeding": "جاري التوزيع",
|
||||||
"stop_seeding": "إيقاف التوزيع",
|
"stop_seeding": "إيقاف التوزيع",
|
||||||
"resume_seeding": "استئناف التوزيع",
|
"resume_seeding": "استئناف التوزيع",
|
||||||
"options": "إدارة"
|
"options": "إدارة"
|
||||||
@@ -228,8 +235,8 @@
|
|||||||
"change": "تحديث",
|
"change": "تحديث",
|
||||||
"notifications": "الإشعارات",
|
"notifications": "الإشعارات",
|
||||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||||
"enable_repack_list_notifications": "عند إضافة حزمة معاد تعبئتها جديدة",
|
"enable_repack_list_notifications": "عند إضافة إصدار معاد جديد",
|
||||||
"real_debrid_api_token_label": "رمز واجهة برمجة تطبيقات Real-Debrid",
|
"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": "عام",
|
||||||
@@ -238,14 +245,14 @@
|
|||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"api_token": "رمز API",
|
"api_token": "رمز API",
|
||||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، محدودة فقط بسرعة اتصالك بالإنترنت.",
|
||||||
"debrid_invalid_token": "رمز API غير صالح",
|
"debrid_invalid_token": "رمز API غير صالح",
|
||||||
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
"real_debrid_free_account_error": "الحساب \"{{username}}\" حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||||
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||||
"save_changes": "حفظ التغييرات",
|
"save_changes": "حفظ التغييرات",
|
||||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL للمصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||||
"validate_download_source": "تحقق",
|
"validate_download_source": "تحقق",
|
||||||
"remove_download_source": "إزالة",
|
"remove_download_source": "إزالة",
|
||||||
"removed_download_sources": "تمت إزالة مصادر التنزيل",
|
"removed_download_sources": "تمت إزالة مصادر التنزيل",
|
||||||
@@ -258,7 +265,7 @@
|
|||||||
"download_count_zero": "لا توجد خيارات تنزيل",
|
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||||
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||||
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
||||||
"download_source_url": "عنوان URL لمصدر التنزيل",
|
"download_source_url": "عنوان مصدر التنزيل",
|
||||||
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||||
"download_source_up_to_date": "محدث",
|
"download_source_up_to_date": "محدث",
|
||||||
"download_source_errored": "خطأ",
|
"download_source_errored": "خطأ",
|
||||||
@@ -278,13 +285,13 @@
|
|||||||
"profile_visibility": "رؤية الملف الشخصي",
|
"profile_visibility": "رؤية الملف الشخصي",
|
||||||
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||||
"required_field": "هذا الحقل مطلوب",
|
"required_field": "هذا الحقل مطلوب",
|
||||||
"source_already_exists": "تمت إضافة هذا المصدر مسبقًا",
|
"source_already_exists": "هذا المصدر مضاف مسبقًا",
|
||||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالح",
|
||||||
"blocked_users": "المستخدمون المحظورون",
|
"blocked_users": "المستخدمون المحظورون",
|
||||||
"user_unblocked": "تم إلغاء حظر المستخدم",
|
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||||
"enable_achievement_notifications": "عند فتح إنجاز",
|
"enable_achievement_notifications": "عند فتح إنجاز",
|
||||||
"launch_minimized": "تشغيل Hydra مصغرًا",
|
"launch_minimized": "تشغيل Hydra مصغرًا",
|
||||||
"disable_nsfw_alert": "تعطيل تنبيه المحتوى غير اللائق",
|
"disable_nsfw_alert": "تعطيل تنبيهات المحتوى غير اللائق",
|
||||||
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
||||||
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
||||||
"account": "الحساب",
|
"account": "الحساب",
|
||||||
@@ -302,18 +309,47 @@
|
|||||||
"become_subscriber": "كن مشتركًا في Hydra Cloud",
|
"become_subscriber": "كن مشتركًا في Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "تم تعطيل التجديد التلقائي",
|
"subscription_renew_cancelled": "تم تعطيل التجديد التلقائي",
|
||||||
"subscription_renews_on": "سيتم تجديد اشتراكك في {{date}}",
|
"subscription_renews_on": "سيتم تجديد اشتراكك في {{date}}",
|
||||||
"bill_sent_until": "سيتم إرسال فاتورتك التالية حتى هذا اليوم"
|
"bill_sent_until": "سيتم إرسال فاتورتك القادمة حتى هذا اليوم",
|
||||||
|
"no_themes": "يبدو أنه ليس لديك أي سمات بعد، لكن لا تقلق، انقر هنا لإنشاء أول تحفة فنية لك.",
|
||||||
|
"editor_tab_code": "الكود",
|
||||||
|
"editor_tab_info": "معلومات",
|
||||||
|
"editor_tab_save": "حفظ",
|
||||||
|
"web_store": "المتجر الإلكتروني",
|
||||||
|
"clear_themes": "مسح",
|
||||||
|
"create_theme": "إنشاء",
|
||||||
|
"create_theme_modal_title": "إنشاء سمة مخصصة",
|
||||||
|
"create_theme_modal_description": "إنشاء سمة جديدة لتخصيص مظهر Hydra",
|
||||||
|
"theme_name": "الاسم",
|
||||||
|
"insert_theme_name": "أدخل اسم السمة",
|
||||||
|
"set_theme": "تعيين السمة",
|
||||||
|
"unset_theme": "إلغاء تعيين السمة",
|
||||||
|
"delete_theme": "حذف السمة",
|
||||||
|
"edit_theme": "تعديل السمة",
|
||||||
|
"delete_all_themes": "حذف جميع السمات",
|
||||||
|
"delete_all_themes_description": "سيؤدي هذا إلى حذف جميع السمات المخصصة الخاصة بك",
|
||||||
|
"delete_theme_description": "سيؤدي هذا إلى حذف السمة {{theme}}",
|
||||||
|
"cancel": "إلغاء",
|
||||||
|
"appearance": "المظهر",
|
||||||
|
"enable_torbox": "تفعيل Torbox",
|
||||||
|
"torbox_description": "TorBox هي خدمة seedbox متميزة تنافس أفضل الخوادم في السوق.",
|
||||||
|
"torbox_account_linked": "تم ربط حساب TorBox",
|
||||||
|
"real_debrid_account_linked": "تم ربط حساب Real-Debrid",
|
||||||
|
"name_min_length": "يجب أن يكون اسم السمة على الأقل 3 أحرف",
|
||||||
|
"import_theme": "استيراد سمة",
|
||||||
|
"import_theme_description": "ستقوم باستيراد {{theme}} من متجر السمات",
|
||||||
|
"error_importing_theme": "خطأ في استيراد السمة",
|
||||||
|
"theme_imported": "تم استيراد السمة بنجاح"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "اكتمل التنزيل",
|
"download_complete": "اكتمل التنزيل",
|
||||||
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||||
"repack_list_updated": "تم تحديث قائمة الحزم المعاد تعبئتها",
|
"repack_list_updated": "تم تحديث قائمة الإصدارات المعادة",
|
||||||
"repack_count_one": "تمت إضافة {{count}} حزمة معاد تعبئتها",
|
"repack_count_one": "تمت إضافة {{count}} إصدار معاد",
|
||||||
"repack_count_other": "تمت إضافة {{count}} حزم معاد تعبئتها",
|
"repack_count_other": "تمت إضافة {{count}} إصدارات معادة",
|
||||||
"new_update_available": "الإصدار {{version}} متوفر",
|
"new_update_available": "الإصدار {{version}} متوفر",
|
||||||
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
||||||
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
||||||
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} آخرين تم فتحهم"
|
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} أخرى تم فتحها"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "فتح Hydra",
|
"open": "فتح Hydra",
|
||||||
@@ -325,7 +361,7 @@
|
|||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "البرامج غير مثبتة",
|
"title": "البرامج غير مثبتة",
|
||||||
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
||||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة لينكس الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "زر الإغلاق"
|
"close": "زر الإغلاق"
|
||||||
@@ -334,16 +370,16 @@
|
|||||||
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} ساعات",
|
"amount_hours": "{{amount}} ساعة",
|
||||||
"amount_minutes": "{{amount}} دقائق",
|
"amount_minutes": "{{amount}} دقيقة",
|
||||||
"last_time_played": "آخر تشغيل {{period}}",
|
"last_time_played": "آخر مرة لعب {{period}}",
|
||||||
"activity": "النشاط الأخير",
|
"activity": "النشاط الأخير",
|
||||||
"library": "المكتبة",
|
"library": "المكتبة",
|
||||||
"total_play_time": "إجمالي وقت اللعب",
|
"total_play_time": "إجمالي وقت اللعب",
|
||||||
"no_recent_activity_title": "همم... لا شيء هنا",
|
"no_recent_activity_title": "لا شيء هنا...",
|
||||||
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
||||||
"display_name": "اسم العرض",
|
"display_name": "اسم العرض",
|
||||||
"saving": "جارٍ الحفظ",
|
"saving": "جاري الحفظ",
|
||||||
"save": "حفظ",
|
"save": "حفظ",
|
||||||
"edit_profile": "تعديل الملف الشخصي",
|
"edit_profile": "تعديل الملف الشخصي",
|
||||||
"saved_successfully": "تم الحفظ بنجاح",
|
"saved_successfully": "تم الحفظ بنجاح",
|
||||||
@@ -352,13 +388,13 @@
|
|||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||||
"sign_out": "تسجيل الخروج",
|
"sign_out": "تسجيل الخروج",
|
||||||
"playing_for": "يلعب لمدة {{amount}}",
|
"playing_for": "جاري اللعب لمدة {{amount}}",
|
||||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية بعد الآن، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||||
"add_friends": "إضافة أصدقاء",
|
"add_friends": "إضافة أصدقاء",
|
||||||
"add": "إضافة",
|
"add": "إضافة",
|
||||||
"friend_code": "رمز الصديق",
|
"friend_code": "رمز الصديق",
|
||||||
"see_profile": "عرض الملف الشخصي",
|
"see_profile": "عرض الملف الشخصي",
|
||||||
"sending": "جارٍ الإرسال",
|
"sending": "جاري الإرسال",
|
||||||
"friend_request_sent": "تم إرسال طلب الصداقة",
|
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||||
"friends": "الأصدقاء",
|
"friends": "الأصدقاء",
|
||||||
"friends_list": "قائمة الأصدقاء",
|
"friends_list": "قائمة الأصدقاء",
|
||||||
@@ -377,19 +413,19 @@
|
|||||||
"blocked_users": "المستخدمون المحظورون",
|
"blocked_users": "المستخدمون المحظورون",
|
||||||
"unblock": "إلغاء الحظر",
|
"unblock": "إلغاء الحظر",
|
||||||
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
||||||
"pending": "قيد الانتظار",
|
"pending": "معلق",
|
||||||
"no_pending_invites": "ليس لديك دعوات معلقة",
|
"no_pending_invites": "ليس لديك دعوات معلقة",
|
||||||
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
||||||
"friend_code_copied": "تم نسخ رمز الصديق",
|
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||||
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
||||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
||||||
"locked_profile": "هذا الملف الشخصي خاص",
|
"locked_profile": "هذا الملف الشخصي خاص",
|
||||||
"image_process_failure": "فشل معالجة الصورة",
|
"image_process_failure": "فشل في معالجة الصورة",
|
||||||
"required_field": "هذا الحقل مطلوب",
|
"required_field": "هذا الحقل مطلوب",
|
||||||
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
||||||
"displayname_max_length": "يجب ألا يتجاوز اسم العرض 50 حرفًا",
|
"displayname_max_length": "يجب أن لا يتجاوز اسم العرض 50 حرفًا",
|
||||||
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
"report_profile": "الإبلاغ عن هذا الملف",
|
||||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف؟",
|
||||||
"report_description": "معلومات إضافية",
|
"report_description": "معلومات إضافية",
|
||||||
"report_description_placeholder": "معلومات إضافية",
|
"report_description_placeholder": "معلومات إضافية",
|
||||||
"report": "الإبلاغ",
|
"report": "الإبلاغ",
|
||||||
@@ -399,32 +435,32 @@
|
|||||||
"report_reason_spam": "بريد عشوائي",
|
"report_reason_spam": "بريد عشوائي",
|
||||||
"report_reason_other": "أخرى",
|
"report_reason_other": "أخرى",
|
||||||
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||||
"your_friend_code": "رمز صديقك:",
|
"your_friend_code": "رمز الصديق الخاص بك:",
|
||||||
"upload_banner": "تحميل بانر",
|
"upload_banner": "رفع بانر",
|
||||||
"uploading_banner": "جارٍ تحميل البانر...",
|
"uploading_banner": "جاري رفع البانر...",
|
||||||
"background_image_updated": "تم تحديث صورة الخلفية",
|
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||||
"stats": "الإحصائيات",
|
"stats": "الإحصائيات",
|
||||||
"achievements": "إنجازات",
|
"achievements": "الإنجازات",
|
||||||
"games": "الألعاب",
|
"games": "الألعاب",
|
||||||
"top_percentile": "ال{{percentile}}% الأعلى",
|
"top_percentile": "الأعلى {{percentile}}%",
|
||||||
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
||||||
"playing": "يلعب {{game}}",
|
"playing": "جاري لعب {{game}}",
|
||||||
"achievements_unlocked": "الإنجازات المفتوحة",
|
"achievements_unlocked": "الإنجازات المفتوحة",
|
||||||
"earned_points": "النقاط المكتسبة",
|
"earned_points": "النقاط المكتسبة",
|
||||||
"show_achievements_on_profile": "عرض إنجازاتك على ملفك الشخصي",
|
"show_achievements_on_profile": "عرض إنجازاتك في ملفك الشخصي",
|
||||||
"show_points_on_profile": "عرض نقاطك المكتسبة على ملفك الشخصي"
|
"show_points_on_profile": "عرض نقاطك المكتسبة في ملفك الشخصي"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "تم فتح الإنجاز",
|
"achievement_unlocked": "تم فتح الإنجاز",
|
||||||
"user_achievements": "إنجازات {{displayName}}",
|
"user_achievements": "إنجازات {{displayName}}",
|
||||||
"your_achievements": "إنجازاتك",
|
"your_achievements": "إنجازاتك",
|
||||||
"unlocked_at": "تم الفتح في: {{date}}",
|
"unlocked_at": "تم الفتح في: {{date}}",
|
||||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لعرض هذا المحتوى",
|
||||||
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
||||||
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
||||||
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
||||||
"achievement_earn_points": "اكسب {{points}} نقطة مع هذا الإنجاز",
|
"achievement_earn_points": "احصل على {{points}} نقاط مع هذا الإنجاز",
|
||||||
"earned_points": "النقاط المكتسبة:",
|
"earned_points": "النقاط المكتسبة:",
|
||||||
"available_points": "النقاط المتاحة:",
|
"available_points": "النقاط المتاحة:",
|
||||||
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
||||||
@@ -434,10 +470,10 @@
|
|||||||
"subscribe_now": "اشترك الآن",
|
"subscribe_now": "اشترك الآن",
|
||||||
"cloud_saving": "حفظ سحابي",
|
"cloud_saving": "حفظ سحابي",
|
||||||
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||||
"animated_profile_picture": "صورة ملف شخصي متحركة",
|
"animated_profile_picture": "صورة ملف متحركة",
|
||||||
"premium_support": "دعم ممتاز",
|
"premium_support": "دعم ممتاز",
|
||||||
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
||||||
"animated_profile_banner": "بانر ملف شخصي متحرك",
|
"animated_profile_banner": "بانر ملف متحرك",
|
||||||
"hydra_cloud": "Hydra Cloud",
|
"hydra_cloud": "Hydra Cloud",
|
||||||
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
||||||
"learn_more": "معرفة المزيد"
|
"learn_more": "معرفة المزيد"
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
"paused": "{{title}} (Спынена)",
|
"paused": "{{title}} (Спынена)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
||||||
"filter": "Фільтар бібліятэкі",
|
"filter": "Фільтар бібліятэкі",
|
||||||
"home": "Галоўная"
|
"home": "Галоўная",
|
||||||
|
"favorites": "Улюбленыя"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Пошук",
|
"search": "Пошук",
|
||||||
"home": "Галоўная",
|
"home": "Галоўная",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "Играта няма избран изпълним файл",
|
"game_has_no_executable": "Играта няма избран изпълним файл",
|
||||||
"sign_in": "Вписване",
|
"sign_in": "Вписване",
|
||||||
"friends": "Приятели",
|
"friends": "Приятели",
|
||||||
"need_help": "Имате нужда от помощ??"
|
"need_help": "Имате нужда от помощ??",
|
||||||
|
"favorites": "Любими игри"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Търсене",
|
"search": "Търсене",
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
"home": "Inici",
|
"home": "Inici",
|
||||||
"queued": "{{title}} (En espera)",
|
"queued": "{{title}} (En espera)",
|
||||||
"game_has_no_executable": "El joc encara no té un executable seleccionat",
|
"game_has_no_executable": "El joc encara no té un executable seleccionat",
|
||||||
"sign_in": "Entra"
|
"sign_in": "Entra",
|
||||||
|
"favorites": "Favorits"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cerca jocs",
|
"search": "Cerca jocs",
|
||||||
|
|
||||||
"home": "Inici",
|
"home": "Inici",
|
||||||
"catalogue": "Catàleg",
|
"catalogue": "Catàleg",
|
||||||
"downloads": "Baixades",
|
"downloads": "Baixades",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
|
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
|
||||||
"sign_in": "Přihlásit se",
|
"sign_in": "Přihlásit se",
|
||||||
"friends": "Přátelé",
|
"friends": "Přátelé",
|
||||||
"need_help": "Potřebujete pomoc?"
|
"need_help": "Potřebujete pomoc?",
|
||||||
|
"favorites": "Oblíbené"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Vyhledat hry",
|
"search": "Vyhledat hry",
|
||||||
|
|||||||
@@ -24,10 +24,12 @@
|
|||||||
"queued": "{{title}} (I køen)",
|
"queued": "{{title}} (I køen)",
|
||||||
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
|
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
|
||||||
"sign_in": "Log ind",
|
"sign_in": "Log ind",
|
||||||
"friends": "Venner"
|
"friends": "Venner",
|
||||||
|
"favorites": "Favoritter"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søg efter spil",
|
"search": "Søg efter spil",
|
||||||
|
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
"home": "Home",
|
"home": "Home",
|
||||||
"queued": "{{title}} (In Warteschlange)",
|
"queued": "{{title}} (In Warteschlange)",
|
||||||
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
||||||
"sign_in": "Anmelden"
|
"sign_in": "Anmelden",
|
||||||
|
"favorites": "Favoriten"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Spiele suchen",
|
"search": "Spiele suchen",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "Game has no executable selected",
|
"game_has_no_executable": "Game has no executable selected",
|
||||||
"sign_in": "Sign in",
|
"sign_in": "Sign in",
|
||||||
"friends": "Friends",
|
"friends": "Friends",
|
||||||
"need_help": "Need help?"
|
"need_help": "Need help?",
|
||||||
|
"favorites": "Favorites"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
@@ -177,6 +178,8 @@
|
|||||||
"manage_files_description": "Manage which files will be backed up and restored",
|
"manage_files_description": "Manage which files will be backed up and restored",
|
||||||
"select_folder": "Select folder",
|
"select_folder": "Select folder",
|
||||||
"backup_from": "Backup from {{date}}",
|
"backup_from": "Backup from {{date}}",
|
||||||
|
"automatic_backup_from": "Automatic backup from {{date}}",
|
||||||
|
"enable_automatic_cloud_sync": "Enable automatic cloud sync",
|
||||||
"custom_backup_location_set": "Custom backup location set",
|
"custom_backup_location_set": "Custom backup location set",
|
||||||
"no_directory_selected": "No directory selected",
|
"no_directory_selected": "No directory selected",
|
||||||
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
||||||
@@ -188,7 +191,9 @@
|
|||||||
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||||
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||||
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
|
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available.",
|
||||||
|
"game_removed_from_favorites": "Game removed from favorites",
|
||||||
|
"game_added_to_favorites": "Game added to favorites"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
@@ -307,10 +312,35 @@
|
|||||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||||
"bill_sent_until": "Your next bill will be sent until this day",
|
"bill_sent_until": "Your next bill will be sent until this day",
|
||||||
|
"no_themes": "Seems like you don't have any themes yet, but no worries, click here to create your first masterpiece.",
|
||||||
|
"editor_tab_code": "Code",
|
||||||
|
"editor_tab_info": "Info",
|
||||||
|
"editor_tab_save": "Save",
|
||||||
|
"web_store": "Web store",
|
||||||
|
"clear_themes": "Clear",
|
||||||
|
"create_theme": "Create",
|
||||||
|
"create_theme_modal_title": "Create custom theme",
|
||||||
|
"create_theme_modal_description": "Create a new theme to customize Hydra's appearance",
|
||||||
|
"theme_name": "Name",
|
||||||
|
"insert_theme_name": "Insert theme name",
|
||||||
|
"set_theme": "Set theme",
|
||||||
|
"unset_theme": "Unset theme",
|
||||||
|
"delete_theme": "Delete theme",
|
||||||
|
"edit_theme": "Edit theme",
|
||||||
|
"delete_all_themes": "Delete all themes",
|
||||||
|
"delete_all_themes_description": "This will delete all your custom themes",
|
||||||
|
"delete_theme_description": "This will delete the theme {{theme}}",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"appearance": "Appearance",
|
||||||
"enable_torbox": "Enable Torbox",
|
"enable_torbox": "Enable Torbox",
|
||||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||||
"torbox_account_linked": "TorBox account linked",
|
"torbox_account_linked": "TorBox account linked",
|
||||||
"real_debrid_account_linked": "Real-Debrid account linked"
|
"real_debrid_account_linked": "Real-Debrid account linked",
|
||||||
|
"name_min_length": "Theme name must be at least 3 characters long",
|
||||||
|
"import_theme": "Import theme",
|
||||||
|
"import_theme_description": "You will import {{theme}} from the theme store",
|
||||||
|
"error_importing_theme": "Error importing theme",
|
||||||
|
"theme_imported": "Theme imported successfully"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||||
"sign_in": "Iniciar sesión",
|
"sign_in": "Iniciar sesión",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "¿Necesitas ayuda?"
|
"need_help": "¿Necesitas ayuda?",
|
||||||
|
"favorites": "Favoritos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar juegos",
|
"search": "Buscar juegos",
|
||||||
@@ -173,6 +174,8 @@
|
|||||||
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
||||||
"select_folder": "Seleccionar carpeta",
|
"select_folder": "Seleccionar carpeta",
|
||||||
"backup_from": "Copia de seguridad de {{date}}",
|
"backup_from": "Copia de seguridad de {{date}}",
|
||||||
|
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
|
||||||
|
"enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube",
|
||||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||||
"clear": "Limpiar",
|
"clear": "Limpiar",
|
||||||
"no_directory_selected": "No se seleccionó un directorio",
|
"no_directory_selected": "No se seleccionó un directorio",
|
||||||
@@ -184,7 +187,13 @@
|
|||||||
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||||
"reset_achievements_title": "¿Estás seguro?",
|
"reset_achievements_title": "¿Estás seguro?",
|
||||||
"reset_achievements_success": "Logros reiniciados exitosamente",
|
"reset_achievements_success": "Logros reiniciados exitosamente",
|
||||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
|
"reset_achievements_error": "Se produjo un error al reiniciar los logros",
|
||||||
|
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
||||||
|
"download_error_not_cached_in_torbox": "Esta descarga no está disponible en Torbox y el estado de descarga del sondeo aún no está disponible.",
|
||||||
|
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||||
|
"game_removed_from_favorites": "Juego removido de favoritos"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
@@ -302,7 +311,37 @@
|
|||||||
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||||
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||||
"update_email": "Actualizar correo",
|
"update_email": "Actualizar correo",
|
||||||
"update_password": "Actualizar contraseña"
|
"update_password": "Actualizar contraseña",
|
||||||
|
"appearance": "Apariencia",
|
||||||
|
"become_subscriber": "Sé Hydra Cloud",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"clear_themes": "Limpiar",
|
||||||
|
"create_theme": "Crear",
|
||||||
|
"create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra",
|
||||||
|
"create_theme_modal_title": "Crear tema personalizado",
|
||||||
|
"delete_all_themes": "Eliminar todos los temas",
|
||||||
|
"delete_all_themes_description": "Esto eliminará todos tus temas personalizados",
|
||||||
|
"delete_theme": "Eliminar tema",
|
||||||
|
"delete_theme_description": "Esto eliminará el tema {{theme}}",
|
||||||
|
"edit_theme": "Editar tema",
|
||||||
|
"editor_tab_code": "Código",
|
||||||
|
"editor_tab_info": "Info",
|
||||||
|
"editor_tab_save": "Guardar",
|
||||||
|
"enable_torbox": "Habilitar Torbox",
|
||||||
|
"error_importing_theme": "Error al importar el tema",
|
||||||
|
"import_theme": "Importar tema",
|
||||||
|
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
|
||||||
|
"insert_theme_name": "Introducí el nombre del tema",
|
||||||
|
"name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo",
|
||||||
|
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.",
|
||||||
|
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
|
||||||
|
"set_theme": "Establecer tema",
|
||||||
|
"theme_imported": "Tema importado exitosamente",
|
||||||
|
"theme_name": "Nombre",
|
||||||
|
"torbox_account_linked": "Cuenta de TorBox vinculada",
|
||||||
|
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
|
||||||
|
"unset_theme": "Desactivar tema",
|
||||||
|
"web_store": "Tienda Web"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"queued": "{{title}} (Järjekorras)",
|
"queued": "{{title}} (Järjekorras)",
|
||||||
"game_has_no_executable": "Mängul pole käivitusfaili valitud",
|
"game_has_no_executable": "Mängul pole käivitusfaili valitud",
|
||||||
"sign_in": "Logi sisse",
|
"sign_in": "Logi sisse",
|
||||||
"friends": "Sõbrad"
|
"friends": "Sõbrad",
|
||||||
|
"favorites": "Lemmikud"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Otsi mänge",
|
"search": "Otsi mänge",
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
"paused": "{{title}} (متوقف شده)",
|
"paused": "{{title}} (متوقف شده)",
|
||||||
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
||||||
"filter": "فیلتر کردن کتابخانه",
|
"filter": "فیلتر کردن کتابخانه",
|
||||||
"home": "خانه"
|
"home": "خانه",
|
||||||
|
"favorites": "علاقهمندیها"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "جستجوی بازیها",
|
"search": "جستجوی بازیها",
|
||||||
"home": "خانه",
|
"home": "خانه",
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
"paused": "{{title}} (En pause)",
|
"paused": "{{title}} (En pause)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||||
"filter": "Filtrer la bibliothèque",
|
"filter": "Filtrer la bibliothèque",
|
||||||
"home": "Page d’accueil"
|
"home": "Page d’accueil",
|
||||||
|
"favorites": "Favoris"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Recherche",
|
"search": "Recherche",
|
||||||
|
|
||||||
"catalogue": "Catalogue",
|
"catalogue": "Catalogue",
|
||||||
"downloads": "Téléchargements",
|
"downloads": "Téléchargements",
|
||||||
"search_results": "Résultats de la recherche",
|
"search_results": "Résultats de la recherche",
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
"paused": "{{title}} (Szünet)",
|
"paused": "{{title}} (Szünet)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||||
"filter": "Könyvtár szűrése",
|
"filter": "Könyvtár szűrése",
|
||||||
"home": "Főoldal"
|
"home": "Főoldal",
|
||||||
|
"favorites": "Kedvenc játékok"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
|
|
||||||
"home": "Főoldal",
|
"home": "Főoldal",
|
||||||
"catalogue": "Katalógus",
|
"catalogue": "Katalógus",
|
||||||
"downloads": "Letöltések",
|
"downloads": "Letöltések",
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
"home": "Beranda",
|
"home": "Beranda",
|
||||||
"queued": "{{title}} (Antrian)",
|
"queued": "{{title}} (Antrian)",
|
||||||
"game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
|
"game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
|
||||||
"sign_in": "Masuk"
|
"sign_in": "Masuk",
|
||||||
|
"favorites": "Favorit"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cari game",
|
"search": "Cari game",
|
||||||
|
|
||||||
"home": "Beranda",
|
"home": "Beranda",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Unduhan",
|
"downloads": "Unduhan",
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
"paused": "{{title}} (In pausa)",
|
"paused": "{{title}} (In pausa)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||||
"filter": "Filtra libreria",
|
"filter": "Filtra libreria",
|
||||||
"home": "Home"
|
"home": "Home",
|
||||||
|
"favorites": "Preferiti"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Catalogo",
|
"catalogue": "Catalogo",
|
||||||
"downloads": "Download",
|
"downloads": "Download",
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
"home": "Басты бет",
|
"home": "Басты бет",
|
||||||
"queued": "{{title}} (Кезекте)",
|
"queued": "{{title}} (Кезекте)",
|
||||||
"game_has_no_executable": "Ойынды іске қосу файлы таңдалмаған",
|
"game_has_no_executable": "Ойынды іске қосу файлы таңдалмаған",
|
||||||
"sign_in": "Кіру"
|
"sign_in": "Кіру",
|
||||||
|
"favorites": "Таңдаулылар"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Іздеу",
|
"search": "Іздеу",
|
||||||
"home": "Басты бет",
|
"home": "Басты бет",
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
"paused": "{{title}} (일시 정지됨)",
|
"paused": "{{title}} (일시 정지됨)",
|
||||||
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
||||||
"filter": "라이브러리 정렬",
|
"filter": "라이브러리 정렬",
|
||||||
"home": "홈"
|
"home": "홈",
|
||||||
|
"favorites": "즐겨찾기"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "게임 검색하기",
|
"search": "게임 검색하기",
|
||||||
"home": "홈",
|
"home": "홈",
|
||||||
|
|||||||
@@ -24,10 +24,12 @@
|
|||||||
"queued": "{{title}} (I køen)",
|
"queued": "{{title}} (I køen)",
|
||||||
"game_has_no_executable": "Spillet har ikke noen kjørbar fil valgt",
|
"game_has_no_executable": "Spillet har ikke noen kjørbar fil valgt",
|
||||||
"sign_in": "Logge inn",
|
"sign_in": "Logge inn",
|
||||||
"friends": "Venner"
|
"friends": "Venner",
|
||||||
|
"favorites": "Favoritter"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søk efter spill",
|
"search": "Søk efter spill",
|
||||||
|
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Nedlastinger",
|
"downloads": "Nedlastinger",
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
"paused": "{{title}} (Gepauzeerd)",
|
"paused": "{{title}} (Gepauzeerd)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filter Bibliotheek",
|
"filter": "Filter Bibliotheek",
|
||||||
"home": "Home"
|
"home": "Home",
|
||||||
|
"favorites": "Favorieten"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Zoek spellen",
|
"search": "Zoek spellen",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Bibliotheek",
|
"catalogue": "Bibliotheek",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
"paused": "{{title}} (Zatrzymano)",
|
"paused": "{{title}} (Zatrzymano)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||||
"filter": "Filtruj biblioteke",
|
"filter": "Filtruj biblioteke",
|
||||||
"home": "Główna"
|
"home": "Główna",
|
||||||
|
"favorites": "Ulubione"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
|
|
||||||
"home": "Główna",
|
"home": "Główna",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Pobrane",
|
"downloads": "Pobrane",
|
||||||
|
|||||||
@@ -26,10 +26,12 @@
|
|||||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||||
"sign_in": "Login",
|
"sign_in": "Login",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "Precisa de ajuda?"
|
"need_help": "Precisa de ajuda?",
|
||||||
|
"favorites": "Favoritos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
|
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"search_results": "Resultados da busca",
|
"search_results": "Resultados da busca",
|
||||||
@@ -163,6 +165,8 @@
|
|||||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||||
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
|
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
|
||||||
"backup_from": "Backup de {{date}}",
|
"backup_from": "Backup de {{date}}",
|
||||||
|
"automatic_backup_from": "Backup automático de {{date}}",
|
||||||
|
"enable_automatic_cloud_sync": "Habilitar sincronização automática na nuvem",
|
||||||
"custom_backup_location_set": "Localização customizada selecionada",
|
"custom_backup_location_set": "Localização customizada selecionada",
|
||||||
"select_folder": "Selecione a pasta",
|
"select_folder": "Selecione a pasta",
|
||||||
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
||||||
@@ -177,7 +181,9 @@
|
|||||||
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
||||||
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||||
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
|
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
|
||||||
|
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||||
|
"game_added_to_favorites": "Jogo adicionado aos favoritos"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@@ -296,10 +302,33 @@
|
|||||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
||||||
|
"no_themes": "Parece que você ainda não tem nenhum tema. Não se preocupe, clique aqui para criar sua primeira obra de arte.",
|
||||||
|
"editor_tab_save": "Salvar",
|
||||||
|
"web_store": "Loja de temas",
|
||||||
|
"clear_themes": "Limpar",
|
||||||
|
"create_theme": "Criar",
|
||||||
|
"create_theme_modal_title": "Criar tema customizado",
|
||||||
|
"create_theme_modal_description": "Criar novo tema para customizar a aparência do Hydra",
|
||||||
|
"theme_name": "Nome",
|
||||||
|
"insert_theme_name": "Insira o nome do tema",
|
||||||
|
"set_theme": "Habilitar tema",
|
||||||
|
"unset_theme": "Desabilitar tema",
|
||||||
|
"delete_theme": "Deletar tema",
|
||||||
|
"edit_theme": "Editar tema",
|
||||||
|
"delete_all_themes": "Deletar todos os temas",
|
||||||
|
"delete_all_themes_description": "Isso irá deletar todos os seus temas",
|
||||||
|
"delete_theme_description": "Isso irá deletar o tema {{theme}}",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"appearance": "Aparência",
|
||||||
"enable_torbox": "Habilitar Torbox",
|
"enable_torbox": "Habilitar Torbox",
|
||||||
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||||
"torbox_account_linked": "Conta do TorBox vinculada",
|
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||||
"real_debrid_account_linked": "Conta Real-Debrid associada"
|
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
||||||
|
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
||||||
|
"import_theme": "Importar tema",
|
||||||
|
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
|
||||||
|
"error_importing_theme": "Erro ao importar tema",
|
||||||
|
"theme_imported": "Tema importado com sucesso"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
|||||||
@@ -25,10 +25,12 @@
|
|||||||
"queued": "{{title}} (Na fila)",
|
"queued": "{{title}} (Na fila)",
|
||||||
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
||||||
"sign_in": "Iniciar sessão",
|
"sign_in": "Iniciar sessão",
|
||||||
"friends": "Amigos"
|
"friends": "Amigos",
|
||||||
|
"favorites": "Favoritos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Procurar jogos",
|
"search": "Procurar jogos",
|
||||||
|
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
"downloads": "Transferências",
|
"downloads": "Transferências",
|
||||||
"search_results": "Resultados da pesquisa",
|
"search_results": "Resultados da pesquisa",
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
"paused": "{{title}} (Pauzat)",
|
"paused": "{{title}} (Pauzat)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Se descarcă...)",
|
"downloading": "{{title}} ({{percentage}} - Se descarcă...)",
|
||||||
"filter": "Filtrează biblioteca",
|
"filter": "Filtrează biblioteca",
|
||||||
"home": "Acasă"
|
"home": "Acasă",
|
||||||
|
"favorites": "Favorite"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Caută jocuri",
|
"search": "Caută jocuri",
|
||||||
|
|
||||||
"home": "Acasă",
|
"home": "Acasă",
|
||||||
"catalogue": "Catalog",
|
"catalogue": "Catalog",
|
||||||
"downloads": "Descărcări",
|
"downloads": "Descărcări",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "Файл запуска игры не выбран",
|
"game_has_no_executable": "Файл запуска игры не выбран",
|
||||||
"sign_in": "Войти",
|
"sign_in": "Войти",
|
||||||
"friends": "Друзья",
|
"friends": "Друзья",
|
||||||
"need_help": "Нужна помощь?"
|
"need_help": "Нужна помощь?",
|
||||||
|
"favorites": "Избранное"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
@@ -177,12 +178,20 @@
|
|||||||
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
|
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
|
||||||
"select_folder": "Выбрать папку",
|
"select_folder": "Выбрать папку",
|
||||||
"backup_from": "Резервная копия от {{date}}",
|
"backup_from": "Резервная копия от {{date}}",
|
||||||
|
"automatic_backup_from": "Автоматическая резервная копия от {{date}}",
|
||||||
|
"enable_automatic_cloud_sync": "Включить автоматическую синхронизацию в облаке",
|
||||||
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
||||||
"no_directory_selected": "Не выбран каталог",
|
"no_directory_selected": "Не выбран каталог",
|
||||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||||
"reset_achievements_title": "Вы уверены?",
|
"reset_achievements_title": "Вы уверены?",
|
||||||
"reset_achievements_success": "Достижения успешно сброшены",
|
"reset_achievements_success": "Достижения успешно сброшены",
|
||||||
"reset_achievements_error": "Не удалось сбросить достижения"
|
"reset_achievements_error": "Не удалось сбросить достижения",
|
||||||
|
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, а опрос статуса загрузки с Real-Debrid пока недоступен.",
|
||||||
|
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и опросить статус загрузки с Torbox пока невозможно.",
|
||||||
|
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||||
|
"game_removed_from_favorites": "Игра удалена из избранного"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Активировать Hydra",
|
"title": "Активировать Hydra",
|
||||||
@@ -300,7 +309,36 @@
|
|||||||
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
||||||
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
||||||
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
|
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня",
|
||||||
|
"no_themes": "Похоже, что у вас еще нет тем, но не волнуйтесь, нажмите здесь, чтобы создать свой первый шедевр",
|
||||||
|
"editor_tab_code": "Код",
|
||||||
|
"editor_tab_info": "Информация",
|
||||||
|
"editor_tab_save": "Сохранить",
|
||||||
|
"web_store": "Веб-магазин",
|
||||||
|
"clear_themes": "Очистить",
|
||||||
|
"create_theme": "Создать",
|
||||||
|
"create_theme_modal_title": "Создать пользовательскую тему",
|
||||||
|
"create_theme_modal_description": "Создать новую тему для настройки внешнего вида Hydra",
|
||||||
|
"theme_name": "Название",
|
||||||
|
"insert_theme_name": "Вставить название темы",
|
||||||
|
"set_theme": "Установить тему",
|
||||||
|
"unset_theme": "Снять тему",
|
||||||
|
"delete_theme": "Удалить тему",
|
||||||
|
"edit_theme": "Редактировать тему",
|
||||||
|
"delete_all_themes": "Удалить все темы",
|
||||||
|
"delete_all_themes_description": "Это удалит все ваши пользовательские темы",
|
||||||
|
"delete_theme_description": "Это приведет к удалению темы {{theme}}",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"appearance": "Внешний вид",
|
||||||
|
"enable_torbox": "Включить Torbox",
|
||||||
|
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
|
||||||
|
"torbox_account_linked": "Аккаунт TorBox привязан",
|
||||||
|
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
|
||||||
|
"name_min_length": "Название темы должно содержать не менее 3 символов",
|
||||||
|
"import_theme": "Импортировать тему",
|
||||||
|
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
|
||||||
|
"error_importing_theme": "Ошибка при импорте темы",
|
||||||
|
"theme_imported": "Тема успешно импортирована"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Загрузка завершена",
|
"download_complete": "Загрузка завершена",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
||||||
"sign_in": "Giriş yap",
|
"sign_in": "Giriş yap",
|
||||||
"friends": "Arkadaşlar",
|
"friends": "Arkadaşlar",
|
||||||
"need_help": "Yardıma mı ihtiyacınız var?"
|
"need_help": "Yardıma mı ihtiyacınız var?",
|
||||||
|
"favorites": "Favoriler"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Oyunları ara",
|
"search": "Oyunları ara",
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
"home": "Головна",
|
"home": "Головна",
|
||||||
"game_has_no_executable": "Не було вибрано файл для запуску гри",
|
"game_has_no_executable": "Не було вибрано файл для запуску гри",
|
||||||
"queued": "{{title}} в черзі",
|
"queued": "{{title}} в черзі",
|
||||||
"sign_in": "Увійти"
|
"sign_in": "Увійти",
|
||||||
|
"favorites": "Улюблені"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Пошук",
|
"search": "Пошук",
|
||||||
|
|
||||||
"home": "Головна",
|
"home": "Головна",
|
||||||
"catalogue": "Каталог",
|
"catalogue": "Каталог",
|
||||||
"downloads": "Завантаження",
|
"downloads": "Завантаження",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"queued": "{{title}} (已加入下载队列)",
|
"queued": "{{title}} (已加入下载队列)",
|
||||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||||
"sign_in": "登入",
|
"sign_in": "登入",
|
||||||
"friends": "好友"
|
"friends": "好友",
|
||||||
|
"favorites": "收藏"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "搜索游戏",
|
"search": "搜索游戏",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import jwt from "jsonwebtoken";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import type { Auth } from "@types";
|
import type { Auth } from "@types";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
|
|
||||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||||
@@ -11,9 +10,7 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!auth) return null;
|
if (!auth) return null;
|
||||||
const payload = jwt.decode(
|
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||||
Crypto.decrypt(auth.accessToken)
|
|
||||||
) as jwt.JwtPayload;
|
|
||||||
|
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,8 @@
|
|||||||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
import { CloudSync } from "@main/services";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import * as tar from "tar";
|
|
||||||
import crypto from "node:crypto";
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import axios from "axios";
|
import { t } from "i18next";
|
||||||
import os from "node:os";
|
import { format } from "date-fns";
|
||||||
import { backupsPath } from "@main/constants";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { normalizePath } from "@main/helpers";
|
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
|
||||||
|
|
||||||
const bundleBackup = async (
|
|
||||||
shop: GameShop,
|
|
||||||
objectId: string,
|
|
||||||
winePrefix: string | null
|
|
||||||
) => {
|
|
||||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
|
||||||
|
|
||||||
// Remove existing backup
|
|
||||||
if (fs.existsSync(backupPath)) {
|
|
||||||
fs.rmSync(backupPath, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
|
||||||
|
|
||||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
|
||||||
|
|
||||||
await tar.create(
|
|
||||||
{
|
|
||||||
gzip: false,
|
|
||||||
file: tarLocation,
|
|
||||||
cwd: backupPath,
|
|
||||||
},
|
|
||||||
["."]
|
|
||||||
);
|
|
||||||
|
|
||||||
return tarLocation;
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadSaveGame = async (
|
const uploadSaveGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -46,61 +10,15 @@ const uploadSaveGame = async (
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
downloadOptionTitle: string | null
|
downloadOptionTitle: string | null
|
||||||
) => {
|
) => {
|
||||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
return CloudSync.uploadSaveGame(
|
||||||
|
|
||||||
const bundleLocation = await bundleBackup(
|
|
||||||
shop,
|
|
||||||
objectId,
|
objectId,
|
||||||
game?.winePrefixPath ?? null
|
shop,
|
||||||
|
downloadOptionTitle,
|
||||||
|
t("backup_from", {
|
||||||
|
ns: "game_details",
|
||||||
|
date: format(new Date(), "dd/MM/yyyy"),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.stat(bundleLocation, async (err, stat) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error("Failed to get zip file stats", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { uploadUrl } = await HydraApi.post<{
|
|
||||||
id: string;
|
|
||||||
uploadUrl: string;
|
|
||||||
}>("/profile/games/artifacts", {
|
|
||||||
artifactLengthInBytes: stat.size,
|
|
||||||
shop,
|
|
||||||
objectId,
|
|
||||||
hostname: os.hostname(),
|
|
||||||
homeDir: normalizePath(app.getPath("home")),
|
|
||||||
downloadOptionTitle,
|
|
||||||
platform: os.platform(),
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.readFile(bundleLocation, async (err, fileBuffer) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error("Failed to read zip file", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios.put(uploadUrl, fileBuffer, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/tar",
|
|
||||||
},
|
|
||||||
onUploadProgress: (progressEvent) => {
|
|
||||||
logger.log(progressEvent);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
|
||||||
`on-upload-complete-${objectId}-${shop}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.rm(bundleLocation, (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error("Failed to remove tar file", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("uploadSaveGame", uploadSaveGame);
|
registerEvent("uploadSaveGame", uploadSaveGame);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import "./catalogue/get-developers";
|
|||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./hardware/check-folder-write-permission";
|
import "./hardware/check-folder-write-permission";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
|
import "./library/add-game-to-favorites";
|
||||||
|
import "./library/remove-game-from-favorites";
|
||||||
import "./library/create-game-shortcut";
|
import "./library/create-game-shortcut";
|
||||||
import "./library/close-game";
|
import "./library/close-game";
|
||||||
import "./library/delete-game-folder";
|
import "./library/delete-game-folder";
|
||||||
@@ -29,11 +31,13 @@ import "./library/remove-game";
|
|||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
import "./library/select-game-wine-prefix";
|
import "./library/select-game-wine-prefix";
|
||||||
import "./library/reset-game-achievements";
|
import "./library/reset-game-achievements";
|
||||||
|
import "./library/toggle-automatic-cloud-sync";
|
||||||
import "./misc/open-checkout";
|
import "./misc/open-checkout";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
import "./misc/get-features";
|
import "./misc/get-features";
|
||||||
import "./misc/show-item-in-folder";
|
import "./misc/show-item-in-folder";
|
||||||
|
import "./misc/get-badges";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
import "./torrenting/pause-game-download";
|
import "./torrenting/pause-game-download";
|
||||||
import "./torrenting/resume-game-download";
|
import "./torrenting/resume-game-download";
|
||||||
@@ -56,6 +60,7 @@ import "./user/get-blocked-users";
|
|||||||
import "./user/block-user";
|
import "./user/block-user";
|
||||||
import "./user/unblock-user";
|
import "./user/unblock-user";
|
||||||
import "./user/get-user-friends";
|
import "./user/get-user-friends";
|
||||||
|
import "./user/get-auth";
|
||||||
import "./user/get-user-stats";
|
import "./user/get-user-stats";
|
||||||
import "./user/report-user";
|
import "./user/report-user";
|
||||||
import "./user/get-unlocked-achievements";
|
import "./user/get-unlocked-achievements";
|
||||||
@@ -75,6 +80,16 @@ import "./cloud-save/upload-save-game";
|
|||||||
import "./cloud-save/delete-game-artifact";
|
import "./cloud-save/delete-game-artifact";
|
||||||
import "./cloud-save/select-game-backup-path";
|
import "./cloud-save/select-game-backup-path";
|
||||||
import "./notifications/publish-new-repacks-notification";
|
import "./notifications/publish-new-repacks-notification";
|
||||||
|
import "./themes/add-custom-theme";
|
||||||
|
import "./themes/delete-custom-theme";
|
||||||
|
import "./themes/get-all-custom-themes";
|
||||||
|
import "./themes/delete-all-custom-themes";
|
||||||
|
import "./themes/update-custom-theme";
|
||||||
|
import "./themes/open-editor-window";
|
||||||
|
import "./themes/get-custom-theme-by-id";
|
||||||
|
import "./themes/get-active-custom-theme";
|
||||||
|
import "./themes/close-editor-window";
|
||||||
|
import "./themes/toggle-custom-theme";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
|
|||||||
25
src/main/events/library/add-game-to-favorites.ts
Normal file
25
src/main/events/library/add-game-to-favorites.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const addGameToFavorites = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
favorite: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to update game favorite status: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("addGameToFavorites", addGameToFavorites);
|
||||||
@@ -5,7 +5,7 @@ import type { Game, GameShop } from "@types";
|
|||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
|
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
@@ -46,9 +46,9 @@ const addGameToLibrary = async (
|
|||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(game);
|
await createGame(game).catch(() => {});
|
||||||
|
|
||||||
createGame(game).catch(() => {});
|
updateLocalUnlockedAchievements(game);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
25
src/main/events/library/remove-game-from-favorites.ts
Normal file
25
src/main/events/library/remove-game-from-favorites.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const removeGameFromFavorites = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
favorite: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to update game favorite status: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("removeGameFromFavorites", removeGameFromFavorites);
|
||||||
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal file
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { levelKeys, gamesSublevel } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const toggleAutomaticCloudSync = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
automaticCloudSync: boolean
|
||||||
|
) => {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
automaticCloudSync,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("toggleAutomaticCloudSync", toggleAutomaticCloudSync);
|
||||||
22
src/main/events/misc/get-badges.ts
Normal file
22
src/main/events/misc/get-badges.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Badge } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
|
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
const language = await db
|
||||||
|
.get<string, string>(levelKeys.language, {
|
||||||
|
valueEncoding: "utf-8",
|
||||||
|
})
|
||||||
|
.then((language) => language || "en");
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
locale: language,
|
||||||
|
});
|
||||||
|
|
||||||
|
return HydraApi.get<Badge[]>(`/badges?${params.toString()}`, null, {
|
||||||
|
needsAuth: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getBadges", getBadges);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { Crypto, HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import type { Auth } from "@types";
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const paymentToken = await HydraApi.post("/auth/payment", {
|
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||||
refreshToken: Crypto.decrypt(auth.refreshToken),
|
refreshToken: auth.refreshToken,
|
||||||
}).then((response) => response.accessToken);
|
}).then((response) => response.accessToken);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|||||||
12
src/main/events/themes/add-custom-theme.ts
Normal file
12
src/main/events/themes/add-custom-theme.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Theme } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
|
||||||
|
const addCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
|
await themesSublevel.put(theme.id, theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("addCustomTheme", addCustomTheme);
|
||||||
11
src/main/events/themes/close-editor-window.ts
Normal file
11
src/main/events/themes/close-editor-window.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const closeEditorWindow = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId?: string
|
||||||
|
) => {
|
||||||
|
WindowManager.closeEditorWindow(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("closeEditorWindow", closeEditorWindow);
|
||||||
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const deleteAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
await themesSublevel.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("deleteAllCustomThemes", deleteAllCustomThemes);
|
||||||
11
src/main/events/themes/delete-custom-theme.ts
Normal file
11
src/main/events/themes/delete-custom-theme.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const deleteCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
await themesSublevel.del(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("deleteCustomTheme", deleteCustomTheme);
|
||||||
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getActiveCustomTheme = async () => {
|
||||||
|
const allThemes = await themesSublevel.values().all();
|
||||||
|
return allThemes.find((theme) => theme.isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getActiveCustomTheme", getActiveCustomTheme);
|
||||||
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
return themesSublevel.values().all();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getAllCustomThemes", getAllCustomThemes);
|
||||||
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getCustomThemeById = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
return themesSublevel.get(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getCustomThemeById", getCustomThemeById);
|
||||||
11
src/main/events/themes/open-editor-window.ts
Normal file
11
src/main/events/themes/open-editor-window.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const openEditorWindow = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
WindowManager.openEditorWindow(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("openEditorWindow", openEditorWindow);
|
||||||
22
src/main/events/themes/toggle-custom-theme.ts
Normal file
22
src/main/events/themes/toggle-custom-theme.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const toggleCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string,
|
||||||
|
isActive: boolean
|
||||||
|
) => {
|
||||||
|
const theme = await themesSublevel.get(themeId);
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
throw new Error("Theme not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await themesSublevel.put(themeId, {
|
||||||
|
...theme,
|
||||||
|
isActive,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("toggleCustomTheme", toggleCustomTheme);
|
||||||
27
src/main/events/themes/update-custom-theme.ts
Normal file
27
src/main/events/themes/update-custom-theme.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
|
||||||
|
const updateCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string,
|
||||||
|
code: string
|
||||||
|
) => {
|
||||||
|
const theme = await themesSublevel.get(themeId);
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
throw new Error("Theme not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await themesSublevel.put(themeId, {
|
||||||
|
...theme,
|
||||||
|
code,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (theme.isActive) {
|
||||||
|
WindowManager.mainWindow?.webContents.send("css-injected", code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateCustomTheme", updateCustomTheme);
|
||||||
@@ -13,7 +13,14 @@ const cancelGameDownload = async (
|
|||||||
|
|
||||||
await DownloadManager.cancelDownload(downloadKey);
|
await DownloadManager.cancelDownload(downloadKey);
|
||||||
|
|
||||||
await downloadsSublevel.del(downloadKey);
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
|
if (!download) return;
|
||||||
|
|
||||||
|
await downloadsSublevel.put(downloadKey, {
|
||||||
|
...download,
|
||||||
|
status: "removed",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const pauseGameSeed = async (
|
|||||||
|
|
||||||
await downloadsSublevel.put(downloadKey, {
|
await downloadsSublevel.put(downloadKey, {
|
||||||
...download,
|
...download,
|
||||||
|
status: "complete",
|
||||||
shouldSeed: false,
|
shouldSeed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ const resumeGameSeed = async (
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
if (!download) return;
|
if (!download) return;
|
||||||
|
|
||||||
await downloadsSublevel.put(levelKeys.game(shop, objectId), {
|
await downloadsSublevel.put(downloadKey, {
|
||||||
...download,
|
...download,
|
||||||
|
status: "seeding",
|
||||||
shouldSeed: true,
|
shouldSeed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,10 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
const getUserPreferences = async () =>
|
const getUserPreferences = async () =>
|
||||||
db
|
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
valueEncoding: "json",
|
||||||
valueEncoding: "json",
|
});
|
||||||
})
|
|
||||||
.then((userPreferences) => {
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
|
||||||
userPreferences.realDebridApiToken = Crypto.decrypt(
|
|
||||||
userPreferences.realDebridApiToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userPreferences?.torBoxApiToken) {
|
|
||||||
userPreferences.torBoxApiToken = Crypto.decrypt(
|
|
||||||
userPreferences.torBoxApiToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return userPreferences;
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEvent("getUserPreferences", getUserPreferences);
|
registerEvent("getUserPreferences", getUserPreferences);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { registerEvent } from "../register-event";
|
|||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
import { patchUserProfile } from "../profile/update-profile";
|
import { patchUserProfile } from "../profile/update-profile";
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
@@ -24,16 +23,6 @@ const updateUserPreferences = async (
|
|||||||
patchUserProfile({ language: preferences.language }).catch(() => {});
|
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.realDebridApiToken) {
|
|
||||||
preferences.realDebridApiToken = Crypto.encrypt(
|
|
||||||
preferences.realDebridApiToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.torBoxApiToken) {
|
|
||||||
preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preferences.downloadsPath) {
|
if (!preferences.downloadsPath) {
|
||||||
preferences.downloadsPath = null;
|
preferences.downloadsPath = null;
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main/events/user/get-auth.ts
Normal file
11
src/main/events/user/get-auth.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getAuth = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||||
|
db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
registerEvent("getAuth", getAuth);
|
||||||
@@ -32,7 +32,7 @@ export const getUnlockedAchievements = async (
|
|||||||
|
|
||||||
return achievementsData
|
return achievementsData
|
||||||
.map((achievementData) => {
|
.map((achievementData) => {
|
||||||
const unlockedAchiementData = unlockedAchievements.find(
|
const unlockedAchievementData = unlockedAchievements.find(
|
||||||
(localAchievement) => {
|
(localAchievement) => {
|
||||||
return (
|
return (
|
||||||
localAchievement.name.toUpperCase() ==
|
localAchievement.name.toUpperCase() ==
|
||||||
@@ -45,11 +45,11 @@ export const getUnlockedAchievements = async (
|
|||||||
? achievementData.icon
|
? achievementData.icon
|
||||||
: achievementData.icongray;
|
: achievementData.icongray;
|
||||||
|
|
||||||
if (unlockedAchiementData) {
|
if (unlockedAchievementData) {
|
||||||
return {
|
return {
|
||||||
...achievementData,
|
...achievementData,
|
||||||
unlocked: true,
|
unlocked: true,
|
||||||
unlockTime: unlockedAchiementData.unlockTime,
|
unlockTime: unlockedAchievementData.unlockTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import updater from "electron-updater";
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import url from "node:url";
|
import url from "node:url";
|
||||||
|
import kill from "kill-port";
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import { logger, WindowManager } from "@main/services";
|
import { logger, WindowManager } from "@main/services";
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
@@ -58,7 +59,7 @@ app.whenReady().then(async () => {
|
|||||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadState();
|
await kill(PythonRPC.RPC_PORT).finally(() => loadState());
|
||||||
|
|
||||||
const language = await db.get<string, string>(levelKeys.language, {
|
const language = await db.get<string, string>(levelKeys.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf-8",
|
||||||
@@ -85,6 +86,29 @@ const handleDeepLinkPath = (uri?: string) => {
|
|||||||
|
|
||||||
if (url.host === "install-source") {
|
if (url.host === "install-source") {
|
||||||
WindowManager.redirect(`settings${url.search}`);
|
WindowManager.redirect(`settings${url.search}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.host === "profile") {
|
||||||
|
const userId = url.searchParams.get("userId");
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
WindowManager.redirect(`profile/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.host === "install-theme") {
|
||||||
|
const themeName = url.searchParams.get("theme");
|
||||||
|
const authorId = url.searchParams.get("authorId");
|
||||||
|
const authorName = url.searchParams.get("authorName");
|
||||||
|
|
||||||
|
if (themeName && authorId && authorName) {
|
||||||
|
WindowManager.redirect(
|
||||||
|
`settings?theme=${themeName}&authorId=${authorId}&authorName=${authorName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error handling deep link", uri, error);
|
logger.error("Error handling deep link", uri, error);
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from "./games";
|
|||||||
export * from "./game-shop-cache";
|
export * from "./game-shop-cache";
|
||||||
export * from "./game-achievements";
|
export * from "./game-achievements";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
|
export * from "./themes";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const levelKeys = {
|
|||||||
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
||||||
user: "user",
|
user: "user",
|
||||||
auth: "auth",
|
auth: "auth",
|
||||||
|
themes: "themes",
|
||||||
gameShopCache: "gameShopCache",
|
gameShopCache: "gameShopCache",
|
||||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||||
`${shop}:${objectId}:${language}`,
|
`${shop}:${objectId}:${language}`,
|
||||||
@@ -13,4 +14,5 @@ export const levelKeys = {
|
|||||||
userPreferences: "userPreferences",
|
userPreferences: "userPreferences",
|
||||||
language: "language",
|
language: "language",
|
||||||
sqliteMigrationDone: "sqliteMigrationDone",
|
sqliteMigrationDone: "sqliteMigrationDone",
|
||||||
|
screenState: "screenState",
|
||||||
};
|
};
|
||||||
|
|||||||
7
src/main/level/sublevels/themes.ts
Normal file
7
src/main/level/sublevels/themes.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Theme } from "@types";
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const themesSublevel = db.sublevel<string, Theme>(levelKeys.themes, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||||
Crypto,
|
|
||||||
DownloadManager,
|
|
||||||
logger,
|
|
||||||
Ludusavi,
|
|
||||||
startMainLoop,
|
|
||||||
} from "./services";
|
|
||||||
import { RealDebridClient } from "./services/download/real-debrid";
|
import { RealDebridClient } from "./services/download/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
@@ -38,13 +32,11 @@ export const loadState = async () => {
|
|||||||
Aria2.spawn();
|
Aria2.spawn();
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
Crypto.decrypt(userPreferences.realDebridApiToken)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userPreferences?.torBoxApiToken) {
|
if (userPreferences?.torBoxApiToken) {
|
||||||
TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken));
|
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
@@ -57,21 +49,17 @@ export const loadState = async () => {
|
|||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
.then((games) => {
|
.then((games) => {
|
||||||
return sortBy(
|
return sortBy(games, "timestamp", "DESC");
|
||||||
games.filter((game) => game.queued),
|
|
||||||
"timestamp",
|
|
||||||
"DESC"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [nextItemOnQueue] = downloads;
|
const [nextItemOnQueue] = downloads.filter((game) => game.queued);
|
||||||
|
|
||||||
const downloadsToSeed = downloads.filter(
|
const downloadsToSeed = downloads.filter(
|
||||||
(download) =>
|
(game) =>
|
||||||
download.shouldSeed &&
|
game.shouldSeed &&
|
||||||
download.downloader === Downloader.Torrent &&
|
game.downloader === Downloader.Torrent &&
|
||||||
download.progress === 1 &&
|
game.progress === 1 &&
|
||||||
download.uri !== null
|
game.uri !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||||
@@ -123,9 +111,7 @@ const migrateFromSqlite = async () => {
|
|||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
...rest,
|
...rest,
|
||||||
realDebridApiToken: realDebridApiToken
|
realDebridApiToken,
|
||||||
? Crypto.encrypt(realDebridApiToken)
|
|
||||||
: null,
|
|
||||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
runAtStartup: rest.runAtStartup === 1,
|
runAtStartup: rest.runAtStartup === 1,
|
||||||
startMinimized: rest.startMinimized === 1,
|
startMinimized: rest.startMinimized === 1,
|
||||||
@@ -144,7 +130,9 @@ const migrateFromSqlite = async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (rest.language) {
|
if (rest.language) {
|
||||||
await db.put(levelKeys.language, rest.language);
|
await db.put<string, string>(levelKeys.language, rest.language, {
|
||||||
|
valueEncoding: "utf-8",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -191,8 +179,8 @@ const migrateFromSqlite = async () => {
|
|||||||
await db.put<string, Auth>(
|
await db.put<string, Auth>(
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
accessToken: Crypto.encrypt(users[0].accessToken),
|
accessToken: users[0].accessToken,
|
||||||
refreshToken: Crypto.encrypt(users[0].refreshToken),
|
refreshToken: users[0].refreshToken,
|
||||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,23 +23,21 @@ const saveAchievementsOnLocal = async (
|
|||||||
return gameAchievementsSublevel
|
return gameAchievementsSublevel
|
||||||
.get(levelKey)
|
.get(levelKey)
|
||||||
.then(async (gameAchievement) => {
|
.then(async (gameAchievement) => {
|
||||||
if (gameAchievement) {
|
await gameAchievementsSublevel.put(levelKey, {
|
||||||
await gameAchievementsSublevel.put(levelKey, {
|
achievements: gameAchievement?.achievements ?? [],
|
||||||
...gameAchievement,
|
unlockedAchievements: unlockedAchievements,
|
||||||
unlockedAchievements: unlockedAchievements,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!sendUpdateEvent) return;
|
if (!sendUpdateEvent) return;
|
||||||
|
|
||||||
return getUnlockedAchievements(objectId, shop, true)
|
return getUnlockedAchievements(objectId, shop, true)
|
||||||
.then((achievements) => {
|
.then((achievements) => {
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
`on-update-achievements-${objectId}-${shop}`,
|
`on-update-achievements-${objectId}-${shop}`,
|
||||||
achievements
|
achievements
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,7 +131,7 @@ export const mergeAchievements = async (
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err! instanceof SubscriptionRequiredError) {
|
if (err instanceof SubscriptionRequiredError) {
|
||||||
achievementsLogger.log(
|
achievementsLogger.log(
|
||||||
"Achievements not synchronized on API due to lack of subscription",
|
"Achievements not synchronized on API due to lack of subscription",
|
||||||
game.objectId,
|
game.objectId,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { parseAchievementFile } from "./parse-achievement-file";
|
|||||||
import { mergeAchievements } from "./merge-achievements";
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
import type { Game, UnlockedAchievement } from "@types";
|
import type { Game, UnlockedAchievement } from "@types";
|
||||||
|
|
||||||
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
export const updateLocalUnlockedAchievements = async (game: Game) => {
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
|
||||||
const achievementFileInsideDirectory =
|
const achievementFileInsideDirectory =
|
||||||
|
|||||||
112
src/main/services/cloud-sync.ts
Normal file
112
src/main/services/cloud-sync.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { levelKeys, gamesSublevel, db } from "@main/level";
|
||||||
|
import { app } from "electron";
|
||||||
|
import path from "node:path";
|
||||||
|
import * as tar from "tar";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import type { GameShop, User } from "@types";
|
||||||
|
import { backupsPath } from "@main/constants";
|
||||||
|
import { HydraApi } from "./hydra-api";
|
||||||
|
import { normalizePath } from "@main/helpers";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
import { WindowManager } from "./window-manager";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Ludusavi } from "./ludusavi";
|
||||||
|
import { isFuture, isToday } from "date-fns";
|
||||||
|
import { SubscriptionRequiredError } from "@shared";
|
||||||
|
|
||||||
|
export class CloudSync {
|
||||||
|
private static async bundleBackup(
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
winePrefix: string | null
|
||||||
|
) {
|
||||||
|
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||||
|
|
||||||
|
// Remove existing backup
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.rmSync(backupPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||||
|
|
||||||
|
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||||
|
|
||||||
|
await tar.create(
|
||||||
|
{
|
||||||
|
gzip: false,
|
||||||
|
file: tarLocation,
|
||||||
|
cwd: backupPath,
|
||||||
|
},
|
||||||
|
["."]
|
||||||
|
);
|
||||||
|
|
||||||
|
return tarLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async uploadSaveGame(
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
downloadOptionTitle: string | null,
|
||||||
|
label?: string
|
||||||
|
) {
|
||||||
|
const hasActiveSubscription = await db
|
||||||
|
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
|
||||||
|
.then((user) => {
|
||||||
|
const expiresAt = user?.subscription?.expiresAt;
|
||||||
|
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasActiveSubscription) {
|
||||||
|
throw new SubscriptionRequiredError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
|
|
||||||
|
const bundleLocation = await this.bundleBackup(
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
game?.winePrefixPath ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
const stat = await fs.promises.stat(bundleLocation);
|
||||||
|
|
||||||
|
const { uploadUrl } = await HydraApi.post<{
|
||||||
|
id: string;
|
||||||
|
uploadUrl: string;
|
||||||
|
}>("/profile/games/artifacts", {
|
||||||
|
artifactLengthInBytes: stat.size,
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
hostname: os.hostname(),
|
||||||
|
homeDir: normalizePath(app.getPath("home")),
|
||||||
|
downloadOptionTitle,
|
||||||
|
platform: os.platform(),
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileBuffer = await fs.promises.readFile(bundleLocation);
|
||||||
|
|
||||||
|
await axios.put(uploadUrl, fileBuffer, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/tar",
|
||||||
|
},
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
logger.log(progressEvent);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-upload-complete-${objectId}-${shop}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.rm(bundleLocation, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error("Failed to remove tar file", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { safeStorage } from "electron";
|
|
||||||
import { logger } from "./logger";
|
|
||||||
|
|
||||||
export class Crypto {
|
|
||||||
public static encrypt(str: string) {
|
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
|
||||||
return safeStorage.encryptString(str).toString("base64");
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"Encrypt method returned raw string because encryption is not available"
|
|
||||||
);
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static decrypt(b64: string) {
|
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
|
||||||
return safeStorage.decryptString(Buffer.from(b64, "base64"));
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"Decrypt method returned raw string because encryption is not available"
|
|
||||||
);
|
|
||||||
|
|
||||||
return b64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,13 @@ import { Downloader, DownloadError } from "@shared";
|
|||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||||
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
|
import {
|
||||||
|
GofileApi,
|
||||||
|
QiwiApi,
|
||||||
|
DatanodesApi,
|
||||||
|
MediafireApi,
|
||||||
|
PixelDrainApi,
|
||||||
|
} from "../hosters";
|
||||||
import { PythonRPC } from "../python-rpc";
|
import { PythonRPC } from "../python-rpc";
|
||||||
import {
|
import {
|
||||||
LibtorrentPayload,
|
LibtorrentPayload,
|
||||||
@@ -219,8 +225,10 @@ export class DownloadManager {
|
|||||||
} as PauseDownloadPayload)
|
} as PauseDownloadPayload)
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
if (downloadKey === this.downloadingGameId) {
|
||||||
this.downloadingGameId = null;
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
|
this.downloadingGameId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(download: Download) {
|
static async resumeDownload(download: Download) {
|
||||||
@@ -228,14 +236,17 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
||||||
await PythonRPC.rpc.post("/action", {
|
await PythonRPC.rpc
|
||||||
action: "cancel",
|
.post("/action", {
|
||||||
game_id: downloadKey,
|
action: "cancel",
|
||||||
});
|
game_id: downloadKey,
|
||||||
|
})
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
.catch((err) => {
|
||||||
|
logger.error("Failed to cancel game download", err);
|
||||||
|
});
|
||||||
|
|
||||||
if (downloadKey === this.downloadingGameId) {
|
if (downloadKey === this.downloadingGameId) {
|
||||||
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
@@ -278,11 +289,12 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
case Downloader.PixelDrain: {
|
case Downloader.PixelDrain: {
|
||||||
const id = download.uri.split("/").pop();
|
const id = download.uri.split("/").pop();
|
||||||
|
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: downloadId,
|
game_id: downloadId,
|
||||||
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
|
url: downloadUrl,
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
TorBoxAddTorrentRequest,
|
TorBoxAddTorrentRequest,
|
||||||
TorBoxRequestLinkRequest,
|
TorBoxRequestLinkRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
import { appVersion } from "@main/constants";
|
||||||
|
|
||||||
export class TorBoxClient {
|
export class TorBoxClient {
|
||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
@@ -18,6 +19,7 @@ export class TorBoxClient {
|
|||||||
baseURL: this.baseURL,
|
baseURL: this.baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${apiToken}`,
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"User-Agent": `Hydra/${appVersion}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,71 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
import { wrapper } from "axios-cookiejar-support";
|
||||||
|
import { CookieJar } from "tough-cookie";
|
||||||
|
|
||||||
export class DatanodesApi {
|
export class DatanodesApi {
|
||||||
private static readonly session = axios.create({});
|
private static readonly jar = new CookieJar();
|
||||||
|
|
||||||
|
private static readonly session = wrapper(
|
||||||
|
axios.create({
|
||||||
|
jar: DatanodesApi.jar,
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
|
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
|
||||||
const parsedUrl = new URL(downloadUrl);
|
try {
|
||||||
const pathSegments = parsedUrl.pathname.split("/");
|
const parsedUrl = new URL(downloadUrl);
|
||||||
|
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
|
||||||
|
const fileCode = pathSegments[0];
|
||||||
|
|
||||||
const fileCode = decodeURIComponent(pathSegments[1]);
|
await this.jar.setCookie("lang=english;", "https://datanodes.to");
|
||||||
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
|
|
||||||
|
|
||||||
const payload = new URLSearchParams({
|
const payload = new URLSearchParams({
|
||||||
op: "download2",
|
op: "download2",
|
||||||
id: fileCode,
|
id: fileCode,
|
||||||
rand: "",
|
method_free: "Free Download >>",
|
||||||
referer: "https://datanodes.to/download",
|
dl: "1",
|
||||||
method_free: "Free Download >>",
|
});
|
||||||
method_premium: "",
|
|
||||||
adblock_detected: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: AxiosResponse = await this.session.post(
|
const response: AxiosResponse = await this.session.post(
|
||||||
"https://datanodes.to/download",
|
"https://datanodes.to/download",
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"User-Agent":
|
||||||
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||||
Host: "datanodes.to",
|
Referer: "https://datanodes.to/download",
|
||||||
Origin: "https://datanodes.to",
|
Origin: "https://datanodes.to",
|
||||||
Referer: "https://datanodes.to/download",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
"User-Agent":
|
},
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
maxRedirects: 0,
|
||||||
},
|
validateStatus: (status: number) => status === 302 || status < 400,
|
||||||
maxRedirects: 0,
|
}
|
||||||
validateStatus: (status: number) => status === 302 || status < 400,
|
);
|
||||||
|
|
||||||
|
if (response.status === 302) {
|
||||||
|
return response.headers["location"];
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status === 302) {
|
if (typeof response.data === "object" && response.data.url) {
|
||||||
return response.headers["location"];
|
return decodeURIComponent(response.data.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent = String(response.data);
|
||||||
|
if (!htmlContent) {
|
||||||
|
throw new Error("Empty response received");
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadLinkRegex = /href=["'](https:\/\/[^"']+)["']/;
|
||||||
|
const downloadLinkMatch = downloadLinkRegex.exec(htmlContent);
|
||||||
|
if (downloadLinkMatch) {
|
||||||
|
return downloadLinkMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Failed to get the download link");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching download URL:", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./gofile";
|
|||||||
export * from "./qiwi";
|
export * from "./qiwi";
|
||||||
export * from "./datanodes";
|
export * from "./datanodes";
|
||||||
export * from "./mediafire";
|
export * from "./mediafire";
|
||||||
|
export * from "./pixeldrain";
|
||||||
|
|||||||
42
src/main/services/hosters/pixeldrain.ts
Normal file
42
src/main/services/hosters/pixeldrain.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export class PixelDrainApi {
|
||||||
|
private static readonly browserHeaders = {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||||
|
Accept:
|
||||||
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
DNT: "1",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Sec-Fetch-User": "?1",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async getDownloadUrl(fileId: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`https://pd.cybar.xyz/${fileId}`, {
|
||||||
|
headers: this.browserHeaders,
|
||||||
|
maxRedirects: 0,
|
||||||
|
validateStatus: (status) =>
|
||||||
|
status === 301 || status === 302 || status === 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.headers.location ||
|
||||||
|
response.status === 301 ||
|
||||||
|
response.status === 302
|
||||||
|
) {
|
||||||
|
return response.headers.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`No redirect URL found (status: ${response.status})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching PixelDrain URL:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import { isFuture, isToday } from "date-fns";
|
|||||||
import { db } from "@main/level";
|
import { db } from "@main/level";
|
||||||
import { levelKeys } from "@main/level/sublevels";
|
import { levelKeys } from "@main/level/sublevels";
|
||||||
import type { Auth, User } from "@types";
|
import type { Auth, User } from "@types";
|
||||||
import { Crypto } from "./crypto";
|
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
@@ -32,8 +31,9 @@ export class HydraApi {
|
|||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||||
|
|
||||||
private static readonly secondsToMilliseconds = (seconds: number) =>
|
private static secondsToMilliseconds(seconds: number) {
|
||||||
seconds * 1000;
|
return seconds * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
private static userAuth: HydraApiUserAuth = {
|
private static userAuth: HydraApiUserAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
@@ -81,8 +81,8 @@ export class HydraApi {
|
|||||||
db.put<string, Auth>(
|
db.put<string, Auth>(
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
accessToken: Crypto.encrypt(accessToken),
|
accessToken,
|
||||||
refreshToken: Crypto.encrypt(refreshToken),
|
refreshToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
@@ -204,12 +204,8 @@ export class HydraApi {
|
|||||||
const user = result.at(1) as User | undefined;
|
const user = result.at(1) as User | undefined;
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: userAuth?.accessToken
|
authToken: userAuth?.accessToken ?? "",
|
||||||
? Crypto.decrypt(userAuth.accessToken)
|
refreshToken: userAuth?.refreshToken ?? "",
|
||||||
: "",
|
|
||||||
refreshToken: userAuth?.refreshToken
|
|
||||||
? Crypto.decrypt(userAuth.refreshToken)
|
|
||||||
: "",
|
|
||||||
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
||||||
subscription: user?.subscription
|
subscription: user?.subscription
|
||||||
? { expiresAt: user.subscription?.expiresAt }
|
? { expiresAt: user.subscription?.expiresAt }
|
||||||
@@ -258,7 +254,7 @@ export class HydraApi {
|
|||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
...auth,
|
...auth,
|
||||||
accessToken: Crypto.encrypt(accessToken),
|
accessToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from "./crypto";
|
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./steam";
|
export * from "./steam";
|
||||||
export * from "./steam-250";
|
export * from "./steam-250";
|
||||||
@@ -8,3 +7,4 @@ export * from "./process-watcher";
|
|||||||
export * from "./main-loop";
|
export * from "./main-loop";
|
||||||
export * from "./hydra-api";
|
export * from "./hydra-api";
|
||||||
export * from "./ludusavi";
|
export * from "./ludusavi";
|
||||||
|
export * from "./cloud-sync";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
? game.playTimeInMilliseconds
|
? game.playTimeInMilliseconds
|
||||||
: localGame.playTimeInMilliseconds;
|
: localGame.playTimeInMilliseconds;
|
||||||
|
|
||||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||||
...localGame,
|
...localGame,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
lastTimePlayed: updatedLastTimePlayed,
|
lastTimePlayed: updatedLastTimePlayed,
|
||||||
@@ -39,7 +39,7 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: steamGame?.name,
|
title: steamGame?.name,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import axios from "axios";
|
|||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { ProcessPayload } from "./download/types";
|
import { ProcessPayload } from "./download/types";
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { CloudSync } from "./cloud-sync";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
|
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
|
||||||
@@ -225,6 +228,18 @@ function onOpenGame(game: Game) {
|
|||||||
|
|
||||||
if (game.remoteId) {
|
if (game.remoteId) {
|
||||||
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
||||||
|
|
||||||
|
if (game.automaticCloudSync) {
|
||||||
|
CloudSync.uploadSaveGame(
|
||||||
|
game.objectId,
|
||||||
|
game.shop,
|
||||||
|
null,
|
||||||
|
t("automatic_backup_from", {
|
||||||
|
ns: "game_details",
|
||||||
|
date: format(new Date(), "dd/MM/yyyy"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -287,6 +302,18 @@ const onCloseGame = (game: Game) => {
|
|||||||
performance.now() - gamePlaytime.lastSyncTick,
|
performance.now() - gamePlaytime.lastSyncTick,
|
||||||
game.lastTimePlayed!
|
game.lastTimePlayed!
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
|
|
||||||
|
if (game.automaticCloudSync) {
|
||||||
|
CloudSync.uploadSaveGame(
|
||||||
|
game.objectId,
|
||||||
|
game.shop,
|
||||||
|
null,
|
||||||
|
t("automatic_backup_from", {
|
||||||
|
ns: "game_details",
|
||||||
|
date: format(new Date(), "dd/MM/yyyy"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
createGame(game).catch(() => {});
|
createGame(game).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,37 @@ import { HydraApi } from "./hydra-api";
|
|||||||
import UserAgent from "user-agents";
|
import UserAgent from "user-agents";
|
||||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { slice, sortBy } from "lodash-es";
|
import { slice, sortBy } from "lodash-es";
|
||||||
import type { UserPreferences } from "@types";
|
import type { ScreenState, UserPreferences } from "@types";
|
||||||
import { AuthPage } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
import { isStaging } from "@main/constants";
|
import { isStaging } from "@main/constants";
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||||
|
|
||||||
|
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||||
|
|
||||||
|
private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions =
|
||||||
|
{
|
||||||
|
width: 1200,
|
||||||
|
height: 720,
|
||||||
|
minWidth: 1024,
|
||||||
|
minHeight: 540,
|
||||||
|
backgroundColor: "#1c1c1c",
|
||||||
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
|
icon,
|
||||||
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
|
titleBarOverlay: {
|
||||||
|
symbolColor: "#DADBE1",
|
||||||
|
color: "#00000000",
|
||||||
|
height: 34,
|
||||||
|
},
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
};
|
||||||
|
|
||||||
private static loadMainWindowURL(hash = "") {
|
private static loadMainWindowURL(hash = "") {
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
@@ -41,29 +65,51 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createMainWindow() {
|
private static async saveScreenConfig({
|
||||||
|
...configScreenWhenClosed
|
||||||
|
}: {
|
||||||
|
x: number | undefined;
|
||||||
|
y: number | undefined;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
isMaximized: boolean;
|
||||||
|
}) {
|
||||||
|
await db.put(levelKeys.screenState, configScreenWhenClosed, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadScreenConfig() {
|
||||||
|
const data = await db.get<string, ScreenState>(levelKeys.screenState, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
return data ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static updateInitialConfig(
|
||||||
|
newConfig: Partial<Electron.BrowserWindowConstructorOptions>
|
||||||
|
) {
|
||||||
|
this.initialConfigInitializationMainWindow = {
|
||||||
|
...this.initialConfigInitializationMainWindow,
|
||||||
|
...newConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createMainWindow() {
|
||||||
if (this.mainWindow) return;
|
if (this.mainWindow) return;
|
||||||
|
|
||||||
this.mainWindow = new BrowserWindow({
|
const { isMaximized = false, ...configWithoutMaximized } =
|
||||||
width: 1200,
|
await this.loadScreenConfig();
|
||||||
height: 720,
|
|
||||||
minWidth: 1024,
|
this.updateInitialConfig(configWithoutMaximized);
|
||||||
minHeight: 540,
|
|
||||||
backgroundColor: "#1c1c1c",
|
this.mainWindow = new BrowserWindow(
|
||||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
this.initialConfigInitializationMainWindow
|
||||||
icon,
|
);
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
|
||||||
titleBarOverlay: {
|
if (isMaximized) {
|
||||||
symbolColor: "#DADBE1",
|
this.mainWindow.maximize();
|
||||||
color: "#151515",
|
}
|
||||||
height: 34,
|
|
||||||
},
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
|
||||||
sandbox: false,
|
|
||||||
},
|
|
||||||
show: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
|
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
|
||||||
(details, callback) => {
|
(details, callback) => {
|
||||||
@@ -141,6 +187,22 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.mainWindow) {
|
||||||
|
const lastBounds = this.mainWindow.getBounds();
|
||||||
|
const isMaximized = this.mainWindow.isMaximized() ?? false;
|
||||||
|
const screenConfig = isMaximized
|
||||||
|
? {
|
||||||
|
x: undefined,
|
||||||
|
y: undefined,
|
||||||
|
height: this.initialConfigInitializationMainWindow.height!,
|
||||||
|
width: this.initialConfigInitializationMainWindow.width!,
|
||||||
|
isMaximized: true,
|
||||||
|
}
|
||||||
|
: { ...lastBounds, isMaximized };
|
||||||
|
|
||||||
|
await this.saveScreenConfig(screenConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
@@ -201,6 +263,87 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static openEditorWindow(themeId: string) {
|
||||||
|
if (this.mainWindow) {
|
||||||
|
const existingWindow = this.editorWindows.get(themeId);
|
||||||
|
if (existingWindow) {
|
||||||
|
if (existingWindow.isMinimized()) {
|
||||||
|
existingWindow.restore();
|
||||||
|
}
|
||||||
|
existingWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorWindow = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 720,
|
||||||
|
minWidth: 600,
|
||||||
|
minHeight: 540,
|
||||||
|
backgroundColor: "#1c1c1c",
|
||||||
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
|
...(process.platform === "linux" ? { icon } : {}),
|
||||||
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
|
titleBarOverlay: {
|
||||||
|
symbolColor: "#DADBE1",
|
||||||
|
color: "#151515",
|
||||||
|
height: 34,
|
||||||
|
},
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editorWindows.set(themeId, editorWindow);
|
||||||
|
|
||||||
|
editorWindow.removeMenu();
|
||||||
|
|
||||||
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
|
editorWindow.loadURL(
|
||||||
|
`${process.env["ELECTRON_RENDERER_URL"]}#/theme-editor?themeId=${themeId}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||||
|
hash: `theme-editor?themeId=${themeId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editorWindow.once("ready-to-show", () => {
|
||||||
|
editorWindow.show();
|
||||||
|
this.mainWindow?.webContents.openDevTools();
|
||||||
|
if (isStaging) {
|
||||||
|
editorWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editorWindow.webContents.on("before-input-event", (event, input) => {
|
||||||
|
if (input.key === "F12") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.mainWindow?.webContents.toggleDevTools();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editorWindow.on("close", () => {
|
||||||
|
this.mainWindow?.webContents.closeDevTools();
|
||||||
|
this.editorWindows.delete(themeId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static closeEditorWindow(themeId?: string) {
|
||||||
|
if (themeId) {
|
||||||
|
const editorWindow = this.editorWindows.get(themeId);
|
||||||
|
if (editorWindow) {
|
||||||
|
editorWindow.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.editorWindows.forEach((editorWindow) => {
|
||||||
|
editorWindow.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static redirect(hash: string) {
|
public static redirect(hash: string) {
|
||||||
if (!this.mainWindow) this.createMainWindow();
|
if (!this.mainWindow) this.createMainWindow();
|
||||||
this.loadMainWindowURL(hash);
|
this.loadMainWindowURL(hash);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
CatalogueSearchPayload,
|
CatalogueSearchPayload,
|
||||||
SeedingStatus,
|
SeedingStatus,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
Theme,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
@@ -100,6 +101,17 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("putDownloadSource", objectIds),
|
ipcRenderer.invoke("putDownloadSource", objectIds),
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
|
toggleAutomaticCloudSync: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
automaticCloudSync: boolean
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
"toggleAutomaticCloudSync",
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
automaticCloudSync
|
||||||
|
),
|
||||||
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
||||||
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
||||||
createGameShortcut: (shop: GameShop, objectId: string) =>
|
createGameShortcut: (shop: GameShop, objectId: string) =>
|
||||||
@@ -110,11 +122,16 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) =>
|
) =>
|
||||||
ipcRenderer.invoke("updateExecutablePath", shop, objectId, executablePath),
|
ipcRenderer.invoke("updateExecutablePath", shop, objectId, executablePath),
|
||||||
|
addGameToFavorites: (shop: GameShop, objectId: string) =>
|
||||||
|
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||||
|
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||||
|
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||||
updateLaunchOptions: (
|
updateLaunchOptions: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
launchOptions: string | null
|
launchOptions: string | null
|
||||||
) => ipcRenderer.invoke("updateLaunchOptions", shop, objectId, launchOptions),
|
) => ipcRenderer.invoke("updateLaunchOptions", shop, objectId, launchOptions),
|
||||||
|
|
||||||
selectGameWinePrefix: (
|
selectGameWinePrefix: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -260,6 +277,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
showItemInFolder: (path: string) =>
|
showItemInFolder: (path: string) =>
|
||||||
ipcRenderer.invoke("showItemInFolder", path),
|
ipcRenderer.invoke("showItemInFolder", path),
|
||||||
getFeatures: () => ipcRenderer.invoke("getFeatures"),
|
getFeatures: () => ipcRenderer.invoke("getFeatures"),
|
||||||
|
getBadges: () => ipcRenderer.invoke("getBadges"),
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
|
||||||
/* Auto update */
|
/* Auto update */
|
||||||
@@ -319,6 +337,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
|
getAuth: () => ipcRenderer.invoke("getAuth"),
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
openAuthWindow: (page: AuthPage) =>
|
openAuthWindow: (page: AuthPage) =>
|
||||||
ipcRenderer.invoke("openAuthWindow", page),
|
ipcRenderer.invoke("openAuthWindow", page),
|
||||||
@@ -342,4 +361,30 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||||
|
|
||||||
|
/* Themes */
|
||||||
|
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),
|
||||||
|
getAllCustomThemes: () => ipcRenderer.invoke("getAllCustomThemes"),
|
||||||
|
deleteAllCustomThemes: () => ipcRenderer.invoke("deleteAllCustomThemes"),
|
||||||
|
deleteCustomTheme: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("deleteCustomTheme", themeId),
|
||||||
|
updateCustomTheme: (themeId: string, code: string) =>
|
||||||
|
ipcRenderer.invoke("updateCustomTheme", themeId, code),
|
||||||
|
getCustomThemeById: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("getCustomThemeById", themeId),
|
||||||
|
getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"),
|
||||||
|
toggleCustomTheme: (themeId: string, isActive: boolean) =>
|
||||||
|
ipcRenderer.invoke("toggleCustomTheme", themeId, isActive),
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
openEditorWindow: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("openEditorWindow", themeId),
|
||||||
|
onCssInjected: (cb: (cssString: string) => void) => {
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent, cssString: string) =>
|
||||||
|
cb(cssString);
|
||||||
|
ipcRenderer.on("css-injected", listener);
|
||||||
|
return () => ipcRenderer.removeListener("css-injected", listener);
|
||||||
|
},
|
||||||
|
closeEditorWindow: (themeId?: string) =>
|
||||||
|
ipcRenderer.invoke("closeEditorWindow", themeId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { downloadSourcesTable } from "./dexie";
|
|||||||
import { useSubscription } from "./hooks/use-subscription";
|
import { useSubscription } from "./hooks/use-subscription";
|
||||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||||
|
|
||||||
|
import { injectCustomCss } from "./helpers";
|
||||||
import "./app.scss";
|
import "./app.scss";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
@@ -233,6 +234,17 @@ export function App() {
|
|||||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
}, [updateRepacks]);
|
}, [updateRepacks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadAndApplyTheme = async () => {
|
||||||
|
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||||
|
|
||||||
|
if (activeTheme?.code) {
|
||||||
|
injectCustomCss(activeTheme.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadAndApplyTheme();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const playAudio = useCallback(() => {
|
const playAudio = useCallback(() => {
|
||||||
const audio = new Audio(achievementSound);
|
const audio = new Audio(achievementSound);
|
||||||
audio.volume = 0.2;
|
audio.volume = 0.2;
|
||||||
@@ -249,6 +261,14 @@ export function App() {
|
|||||||
};
|
};
|
||||||
}, [playAudio]);
|
}, [playAudio]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
||||||
|
injectCustomCss(cssString);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
dispatch(closeToast());
|
dispatch(closeToast());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonIcon } from "@primer/octicons-react";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
import "./avatar.scss";
|
import "./avatar.scss";
|
||||||
|
|
||||||
@@ -14,11 +15,18 @@ export interface AvatarProps
|
|||||||
src?: string | null;
|
src?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
|
export function Avatar({ size, alt, src, className, ...props }: AvatarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="profile-avatar" style={{ width: size, height: size }}>
|
<div className="profile-avatar" style={{ width: size, height: size }}>
|
||||||
{src ? (
|
{src ? (
|
||||||
<img className="profile-avatar__image" alt={alt} src={src} {...props} />
|
<img
|
||||||
|
className={cn("profile-avatar__image", className)}
|
||||||
|
alt={alt}
|
||||||
|
src={src}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PersonIcon size={size * 0.7} />
|
<PersonIcon size={size * 0.7} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ export interface BackdropProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
export function Backdrop({
|
||||||
|
isClosing = false,
|
||||||
|
children,
|
||||||
|
}: Readonly<BackdropProps>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("backdrop", {
|
className={cn("backdrop", {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function Button({
|
|||||||
theme = "primary",
|
theme = "primary",
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: Readonly<ButtonProps>) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface CheckboxFieldProps
|
|||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
label: string;
|
label: string | React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
||||||
|
|||||||
50
src/renderer/src/components/sidebar/sidebar-game-item.tsx
Normal file
50
src/renderer/src/components/sidebar/sidebar-game-item.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
import { LibraryGame } from "@types";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
interface SidebarGameItemProps {
|
||||||
|
game: LibraryGame;
|
||||||
|
handleSidebarGameClick: (event: React.MouseEvent, game: LibraryGame) => void;
|
||||||
|
getGameTitle: (game: LibraryGame) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarGameItem({
|
||||||
|
game,
|
||||||
|
handleSidebarGameClick,
|
||||||
|
getGameTitle,
|
||||||
|
}: Readonly<SidebarGameItemProps>) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={game.id}
|
||||||
|
className={cn("sidebar__menu-item", {
|
||||||
|
"sidebar__menu-item--active":
|
||||||
|
location.pathname === `/game/${game.shop}/${game.objectId}`,
|
||||||
|
"sidebar__menu-item--muted": game.download?.status === "removed",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sidebar__menu-item-button"
|
||||||
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
|
>
|
||||||
|
{game.iconUrl ? (
|
||||||
|
<img
|
||||||
|
className="sidebar__game-icon"
|
||||||
|
src={game.iconUrl}
|
||||||
|
alt={game.title}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SteamLogo className="sidebar__game-icon" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="sidebar__menu-item-button-label">
|
||||||
|
{getGameTitle(game)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
&__content {
|
&__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
@@ -104,13 +106,14 @@
|
|||||||
&__container {
|
&__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__section {
|
&__section {
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
padding-bottom: globals.$spacing-unit;
|
padding-bottom: globals.$spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import "./sidebar.scss";
|
|||||||
|
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||||
|
import { SidebarGameItem } from "./sidebar-game-item";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@@ -167,6 +167,10 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const favoriteGames = useMemo(() => {
|
||||||
|
return sortedLibrary.filter((game) => game.favorite);
|
||||||
|
}, [sortedLibrary]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
@@ -206,6 +210,23 @@ export function Sidebar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{favoriteGames.length > 0 && (
|
||||||
|
<section className="sidebar__section">
|
||||||
|
<small className="sidebar__section-title">{t("favorites")}</small>
|
||||||
|
|
||||||
|
<ul className="sidebar__menu">
|
||||||
|
{favoriteGames.map((game) => (
|
||||||
|
<SidebarGameItem
|
||||||
|
key={game.id}
|
||||||
|
game={game}
|
||||||
|
handleSidebarGameClick={handleSidebarGameClick}
|
||||||
|
getGameTitle={getGameTitle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="sidebar__section">
|
<section className="sidebar__section">
|
||||||
<small className="sidebar__section-title">{t("my_library")}</small>
|
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||||
|
|
||||||
@@ -217,39 +238,16 @@ export function Sidebar() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ul className="sidebar__menu">
|
<ul className="sidebar__menu">
|
||||||
{filteredLibrary.map((game) => (
|
{filteredLibrary
|
||||||
<li
|
.filter((game) => !game.favorite)
|
||||||
key={game.id}
|
.map((game) => (
|
||||||
className={cn("sidebar__menu-item", {
|
<SidebarGameItem
|
||||||
"sidebar__menu-item--active":
|
key={game.id}
|
||||||
location.pathname ===
|
game={game}
|
||||||
`/game/${game.shop}/${game.objectId}`,
|
handleSidebarGameClick={handleSidebarGameClick}
|
||||||
"sidebar__menu-item--muted":
|
getGameTitle={getGameTitle}
|
||||||
game.download?.status === "removed",
|
/>
|
||||||
})}
|
))}
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sidebar__menu-item-button"
|
|
||||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
|
||||||
>
|
|
||||||
{game.iconUrl ? (
|
|
||||||
<img
|
|
||||||
className="sidebar__game-icon"
|
|
||||||
src={game.iconUrl}
|
|
||||||
alt={game.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SteamLogo className="sidebar__game-icon" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="sidebar__menu-item-button-label">
|
|
||||||
{getGameTitle(game)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$dark-background-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px globals.$border-color;
|
||||||
right: 16px;
|
right: calc(globals.$spacing-unit * 2);
|
||||||
bottom: 26px + globals.$spacing-unit;
|
// 28px is the height of the bottom panel
|
||||||
|
bottom: calc(28px + globals.$spacing-unit * 2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
|
|
||||||
export const VERSION_CODENAME = "Spectre";
|
export const VERSION_CODENAME = "Polychrome";
|
||||||
|
|
||||||
export const DOWNLOADER_NAME = {
|
export const DOWNLOADER_NAME = {
|
||||||
[Downloader.RealDebrid]: "Real-Debrid",
|
[Downloader.RealDebrid]: "Real-Debrid",
|
||||||
@@ -14,3 +14,5 @@ export const DOWNLOADER_NAME = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
|
export const THEME_WEB_STORE_URL = "https://hydrathemes.shop";
|
||||||
|
|||||||
@@ -9,20 +9,32 @@ export interface SettingsContext {
|
|||||||
updateUserPreferences: (values: Partial<UserPreferences>) => Promise<void>;
|
updateUserPreferences: (values: Partial<UserPreferences>) => Promise<void>;
|
||||||
setCurrentCategoryIndex: React.Dispatch<React.SetStateAction<number>>;
|
setCurrentCategoryIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||||
clearSourceUrl: () => void;
|
clearSourceUrl: () => void;
|
||||||
|
clearTheme: () => void;
|
||||||
sourceUrl: string | null;
|
sourceUrl: string | null;
|
||||||
currentCategoryIndex: number;
|
currentCategoryIndex: number;
|
||||||
blockedUsers: UserBlocks["blocks"];
|
blockedUsers: UserBlocks["blocks"];
|
||||||
fetchBlockedUsers: () => Promise<void>;
|
fetchBlockedUsers: () => Promise<void>;
|
||||||
|
appearance: {
|
||||||
|
theme: string | null;
|
||||||
|
authorId: string | null;
|
||||||
|
authorName: string | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsContext = createContext<SettingsContext>({
|
export const settingsContext = createContext<SettingsContext>({
|
||||||
updateUserPreferences: async () => {},
|
updateUserPreferences: async () => {},
|
||||||
setCurrentCategoryIndex: () => {},
|
setCurrentCategoryIndex: () => {},
|
||||||
clearSourceUrl: () => {},
|
clearSourceUrl: () => {},
|
||||||
|
clearTheme: () => {},
|
||||||
sourceUrl: null,
|
sourceUrl: null,
|
||||||
currentCategoryIndex: 0,
|
currentCategoryIndex: 0,
|
||||||
blockedUsers: [],
|
blockedUsers: [],
|
||||||
fetchBlockedUsers: async () => {},
|
fetchBlockedUsers: async () => {},
|
||||||
|
appearance: {
|
||||||
|
theme: null,
|
||||||
|
authorId: null,
|
||||||
|
authorName: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Provider } = settingsContext;
|
const { Provider } = settingsContext;
|
||||||
@@ -34,15 +46,26 @@ export interface SettingsContextProviderProps {
|
|||||||
|
|
||||||
export function SettingsContextProvider({
|
export function SettingsContextProvider({
|
||||||
children,
|
children,
|
||||||
}: SettingsContextProviderProps) {
|
}: Readonly<SettingsContextProviderProps>) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [sourceUrl, setSourceUrl] = useState<string | null>(null);
|
const [sourceUrl, setSourceUrl] = useState<string | null>(null);
|
||||||
|
const [appearance, setAppearance] = useState<{
|
||||||
|
theme: string | null;
|
||||||
|
authorId: string | null;
|
||||||
|
authorName: string | null;
|
||||||
|
}>({
|
||||||
|
theme: null,
|
||||||
|
authorId: null,
|
||||||
|
authorName: null,
|
||||||
|
});
|
||||||
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
||||||
|
|
||||||
const [blockedUsers, setBlockedUsers] = useState<UserBlocks["blocks"]>([]);
|
const [blockedUsers, setBlockedUsers] = useState<UserBlocks["blocks"]>([]);
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const defaultSourceUrl = searchParams.get("urls");
|
const defaultSourceUrl = searchParams.get("urls");
|
||||||
|
const defaultAppearanceTheme = searchParams.get("theme");
|
||||||
|
const defaultAppearanceAuthorId = searchParams.get("authorId");
|
||||||
|
const defaultAppearanceAuthorName = searchParams.get("authorName");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sourceUrl) setCurrentCategoryIndex(2);
|
if (sourceUrl) setCurrentCategoryIndex(2);
|
||||||
@@ -54,6 +77,36 @@ export function SettingsContextProvider({
|
|||||||
}
|
}
|
||||||
}, [defaultSourceUrl]);
|
}, [defaultSourceUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appearance.theme) setCurrentCategoryIndex(3);
|
||||||
|
}, [appearance.theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
defaultAppearanceTheme &&
|
||||||
|
defaultAppearanceAuthorId &&
|
||||||
|
defaultAppearanceAuthorName
|
||||||
|
) {
|
||||||
|
setAppearance({
|
||||||
|
theme: defaultAppearanceTheme,
|
||||||
|
authorId: defaultAppearanceAuthorId,
|
||||||
|
authorName: defaultAppearanceAuthorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
defaultAppearanceTheme,
|
||||||
|
defaultAppearanceAuthorId,
|
||||||
|
defaultAppearanceAuthorName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const clearTheme = useCallback(() => {
|
||||||
|
setAppearance({
|
||||||
|
theme: null,
|
||||||
|
authorId: null,
|
||||||
|
authorName: null,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchBlockedUsers = useCallback(async () => {
|
const fetchBlockedUsers = useCallback(async () => {
|
||||||
const blockedUsers = await window.electron.getBlockedUsers(12, 0);
|
const blockedUsers = await window.electron.getBlockedUsers(12, 0);
|
||||||
setBlockedUsers(blockedUsers.blocks);
|
setBlockedUsers(blockedUsers.blocks);
|
||||||
@@ -79,9 +132,11 @@ export function SettingsContextProvider({
|
|||||||
setCurrentCategoryIndex,
|
setCurrentCategoryIndex,
|
||||||
clearSourceUrl,
|
clearSourceUrl,
|
||||||
fetchBlockedUsers,
|
fetchBlockedUsers,
|
||||||
|
clearTheme,
|
||||||
currentCategoryIndex,
|
currentCategoryIndex,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
blockedUsers,
|
blockedUsers,
|
||||||
|
appearance,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { darkenColor } from "@renderer/helpers";
|
import { darkenColor } from "@renderer/helpers";
|
||||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||||
import type { UserProfile, UserStats } from "@types";
|
import type { Badge, UserProfile, UserStats } from "@types";
|
||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
|
|
||||||
import { createContext, useCallback, useEffect, useState } from "react";
|
import { createContext, useCallback, useEffect, useState } from "react";
|
||||||
@@ -16,6 +16,7 @@ export interface UserProfileContext {
|
|||||||
getUserProfile: () => Promise<void>;
|
getUserProfile: () => Promise<void>;
|
||||||
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
|
badges: Badge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
||||||
@@ -28,6 +29,7 @@ export const userProfileContext = createContext<UserProfileContext>({
|
|||||||
getUserProfile: async () => {},
|
getUserProfile: async () => {},
|
||||||
setSelectedBackgroundImage: () => {},
|
setSelectedBackgroundImage: () => {},
|
||||||
backgroundImage: "",
|
backgroundImage: "",
|
||||||
|
badges: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Provider } = userProfileContext;
|
const { Provider } = userProfileContext;
|
||||||
@@ -41,12 +43,13 @@ export interface UserProfileContextProviderProps {
|
|||||||
export function UserProfileContextProvider({
|
export function UserProfileContextProvider({
|
||||||
children,
|
children,
|
||||||
userId,
|
userId,
|
||||||
}: UserProfileContextProviderProps) {
|
}: Readonly<UserProfileContextProviderProps>) {
|
||||||
const { userDetails } = useAppSelector((state) => state.userDetails);
|
const { userDetails } = useAppSelector((state) => state.userDetails);
|
||||||
|
|
||||||
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
||||||
|
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
|
const [badges, setBadges] = useState<Badge[]>([]);
|
||||||
const [heroBackground, setHeroBackground] = useState(
|
const [heroBackground, setHeroBackground] = useState(
|
||||||
DEFAULT_USER_PROFILE_BACKGROUND
|
DEFAULT_USER_PROFILE_BACKGROUND
|
||||||
);
|
);
|
||||||
@@ -101,12 +104,18 @@ export function UserProfileContextProvider({
|
|||||||
});
|
});
|
||||||
}, [navigate, getUserStats, showErrorToast, userId, t]);
|
}, [navigate, getUserStats, showErrorToast, userId, t]);
|
||||||
|
|
||||||
|
const getBadges = useCallback(async () => {
|
||||||
|
const badges = await window.electron.getBadges();
|
||||||
|
setBadges(badges);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUserProfile(null);
|
setUserProfile(null);
|
||||||
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
|
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
|
||||||
|
|
||||||
getUserProfile();
|
getUserProfile();
|
||||||
}, [getUserProfile]);
|
getBadges();
|
||||||
|
}, [getUserProfile, getBadges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider
|
<Provider
|
||||||
@@ -118,6 +127,7 @@ export function UserProfileContextProvider({
|
|||||||
setSelectedBackgroundImage,
|
setSelectedBackgroundImage,
|
||||||
backgroundImage: getBackgroundImageUrl(),
|
backgroundImage: getBackgroundImageUrl(),
|
||||||
userStats,
|
userStats,
|
||||||
|
badges,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
32
src/renderer/src/declaration.d.ts
vendored
32
src/renderer/src/declaration.d.ts
vendored
@@ -29,6 +29,9 @@ import type {
|
|||||||
LibraryGame,
|
LibraryGame,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
TorBoxUser,
|
TorBoxUser,
|
||||||
|
Theme,
|
||||||
|
Badge,
|
||||||
|
Auth,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
@@ -85,6 +88,11 @@ declare global {
|
|||||||
getDevelopers: () => Promise<string[]>;
|
getDevelopers: () => Promise<string[]>;
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
|
toggleAutomaticCloudSync: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
automaticCloudSync: boolean
|
||||||
|
) => Promise<void>;
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -96,6 +104,11 @@ declare global {
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
addGameToFavorites: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
|
removeGameFromFavorites: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) => Promise<void>;
|
||||||
updateLaunchOptions: (
|
updateLaunchOptions: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -211,6 +224,7 @@ declare global {
|
|||||||
) => Promise<Electron.OpenDialogReturnValue>;
|
) => Promise<Electron.OpenDialogReturnValue>;
|
||||||
showItemInFolder: (path: string) => Promise<void>;
|
showItemInFolder: (path: string) => Promise<void>;
|
||||||
getFeatures: () => Promise<string[]>;
|
getFeatures: () => Promise<string[]>;
|
||||||
|
getBadges: () => Promise<Badge[]>;
|
||||||
platform: NodeJS.Platform;
|
platform: NodeJS.Platform;
|
||||||
|
|
||||||
/* Auto update */
|
/* Auto update */
|
||||||
@@ -221,6 +235,7 @@ declare global {
|
|||||||
restartAndInstallUpdate: () => Promise<void>;
|
restartAndInstallUpdate: () => Promise<void>;
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
|
getAuth: () => Promise<Auth | null>;
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
openAuthWindow: (page: AuthPage) => Promise<void>;
|
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||||
getSessionHash: () => Promise<string | null>;
|
getSessionHash: () => Promise<string | null>;
|
||||||
@@ -274,6 +289,23 @@ declare global {
|
|||||||
|
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||||
|
|
||||||
|
/* Themes */
|
||||||
|
addCustomTheme: (theme: Theme) => Promise<void>;
|
||||||
|
getAllCustomThemes: () => Promise<Theme[]>;
|
||||||
|
deleteAllCustomThemes: () => Promise<void>;
|
||||||
|
deleteCustomTheme: (themeId: string) => Promise<void>;
|
||||||
|
updateCustomTheme: (themeId: string, code: string) => Promise<void>;
|
||||||
|
getCustomThemeById: (themeId: string) => Promise<Theme | null>;
|
||||||
|
getActiveCustomTheme: () => Promise<Theme | null>;
|
||||||
|
toggleCustomTheme: (themeId: string, isActive: boolean) => Promise<void>;
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
openEditorWindow: (themeId: string) => Promise<void>;
|
||||||
|
onCssInjected: (
|
||||||
|
cb: (cssString: string) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
closeEditorWindow: (themeId?: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const toastSlice = createSlice({
|
|||||||
state.title = action.payload.title;
|
state.title = action.payload.title;
|
||||||
state.message = action.payload.message;
|
state.message = action.payload.message;
|
||||||
state.type = action.payload.type;
|
state.type = action.payload.type;
|
||||||
state.duration = action.payload.duration ?? 5000;
|
state.duration = action.payload.duration ?? 2000;
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
},
|
},
|
||||||
closeToast: (state) => {
|
closeToast: (state) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
|
import { THEME_WEB_STORE_URL } from "./constants";
|
||||||
|
|
||||||
export const formatDownloadProgress = (
|
export const formatDownloadProgress = (
|
||||||
progress?: number,
|
progress?: number,
|
||||||
@@ -53,3 +54,36 @@ export const buildGameAchievementPath = (
|
|||||||
|
|
||||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||||
new Color(color).darken(amount).alpha(alpha).toString();
|
new Color(color).darken(amount).alpha(alpha).toString();
|
||||||
|
|
||||||
|
export const injectCustomCss = (css: string) => {
|
||||||
|
try {
|
||||||
|
const currentCustomCss = document.getElementById("custom-css");
|
||||||
|
if (currentCustomCss) {
|
||||||
|
currentCustomCss.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (css.startsWith(THEME_WEB_STORE_URL)) {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.id = "custom-css";
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = css;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
} else {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = "custom-css";
|
||||||
|
style.textContent = `
|
||||||
|
${css}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("failed to inject custom css:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeCustomCss = () => {
|
||||||
|
const currentCustomCss = document.getElementById("custom-css");
|
||||||
|
if (currentCustomCss) {
|
||||||
|
currentCustomCss.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function useDownload() {
|
|||||||
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
||||||
await window.electron.pauseGameDownload(shop, objectId);
|
await window.electron.pauseGameDownload(shop, objectId);
|
||||||
await updateLibrary();
|
await updateLibrary();
|
||||||
dispatch(clearDownload());
|
if (lastPacket?.gameId === `${shop}:${objectId}`) dispatch(clearDownload());
|
||||||
};
|
};
|
||||||
|
|
||||||
const resumeDownload = async (shop: GameShop, objectId: string) => {
|
const resumeDownload = async (shop: GameShop, objectId: string) => {
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
enum Feature {
|
enum Feature {
|
||||||
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
|
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
|
||||||
|
Torbox = "TORBOX",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFeature() {
|
export function useFeature() {
|
||||||
|
const [features, setFeatures] = useState<string[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electron.getFeatures().then((features) => {
|
window.electron.getFeatures().then((features) => {
|
||||||
localStorage.setItem("features", JSON.stringify(features || []));
|
localStorage.setItem("features", JSON.stringify(features || []));
|
||||||
|
setFeatures(features || []);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isFeatureEnabled = (feature: Feature) => {
|
const isFeatureEnabled = (feature: Feature) => {
|
||||||
const features = JSON.parse(localStorage.getItem("features") || "[]");
|
if (!features) {
|
||||||
|
const features = JSON.parse(localStorage.getItem("features") ?? "[]");
|
||||||
|
return features.includes(feature);
|
||||||
|
}
|
||||||
|
|
||||||
return features.includes(feature);
|
return features.includes(feature);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import "@fontsource/noto-sans/500.css";
|
|||||||
import "@fontsource/noto-sans/700.css";
|
import "@fontsource/noto-sans/700.css";
|
||||||
|
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
import "react-tooltip/dist/react-tooltip.css";
|
||||||
|
|
||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
|
|
||||||
@@ -18,23 +19,17 @@ import { store } from "./store";
|
|||||||
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
|
|
||||||
import { SuspenseWrapper } from "./components";
|
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { addCookieInterceptor } from "./cookies";
|
import { addCookieInterceptor } from "./cookies";
|
||||||
|
|
||||||
const Home = React.lazy(() => import("./pages/home/home"));
|
|
||||||
const GameDetails = React.lazy(
|
|
||||||
() => import("./pages/game-details/game-details")
|
|
||||||
);
|
|
||||||
const Downloads = React.lazy(() => import("./pages/downloads/downloads"));
|
|
||||||
const Settings = React.lazy(() => import("./pages/settings/settings"));
|
|
||||||
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
|
|
||||||
const Profile = React.lazy(() => import("./pages/profile/profile"));
|
|
||||||
const Achievements = React.lazy(
|
|
||||||
() => import("./pages/achievements/achievements")
|
|
||||||
);
|
|
||||||
|
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import Catalogue from "./pages/catalogue/catalogue";
|
||||||
|
import Home from "./pages/home/home";
|
||||||
|
import Downloads from "./pages/downloads/downloads";
|
||||||
|
import GameDetails from "./pages/game-details/game-details";
|
||||||
|
import Settings from "./pages/settings/settings";
|
||||||
|
import Profile from "./pages/profile/profile";
|
||||||
|
import Achievements from "./pages/achievements/achievements";
|
||||||
|
import ThemeEditor from "./pages/theme-editor/theme-editor";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
||||||
@@ -79,32 +74,16 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<App />}>
|
<Route element={<App />}>
|
||||||
<Route path="/" element={<SuspenseWrapper Component={Home} />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route
|
<Route path="/catalogue" element={<Catalogue />} />
|
||||||
path="/catalogue"
|
<Route path="/downloads" element={<Downloads />} />
|
||||||
element={<SuspenseWrapper Component={Catalogue} />}
|
<Route path="/game/:shop/:objectId" element={<GameDetails />} />
|
||||||
/>
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route
|
<Route path="/profile/:userId" element={<Profile />} />
|
||||||
path="/downloads"
|
<Route path="/achievements" element={<Achievements />} />
|
||||||
element={<SuspenseWrapper Component={Downloads} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/game/:shop/:objectId"
|
|
||||||
element={<SuspenseWrapper Component={GameDetails} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/settings"
|
|
||||||
element={<SuspenseWrapper Component={Settings} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/profile/:userId"
|
|
||||||
element={<SuspenseWrapper Component={Profile} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/achievements"
|
|
||||||
element={<SuspenseWrapper Component={Achievements} />}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/theme-editor" element={<ThemeEditor />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ $logo-max-width: 200px;
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: globals.$spacing-unit / 2;
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
color: globals.$body-color;
|
color: globals.$body-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user