mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +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
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Lint
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Release
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.1.5",
|
||||
"version": "3.3.0",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -36,6 +36,7 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@fontsource/noto-sans": "^5.1.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
@@ -43,6 +44,7 @@
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"axios-cookiejar-support": "^5.0.5",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"classic-level": "^2.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -59,6 +61,7 @@
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kill-port": "^2.0.1",
|
||||
"knex": "^3.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
@@ -69,9 +72,11 @@
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-redux": "^9.1.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"sound-play": "^1.1.0",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tar": "^7.4.3",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"user-agents": "^1.1.387",
|
||||
"yaml": "^2.6.1",
|
||||
"yup": "^1.5.0",
|
||||
|
||||
@@ -159,6 +159,8 @@ def action():
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.pause_download()
|
||||
|
||||
if downloading_game_id == game_id:
|
||||
downloading_game_id = -1
|
||||
elif action == 'cancel':
|
||||
downloader = downloads.get(game_id)
|
||||
|
||||
@@ -107,7 +107,10 @@ const copyAria2Macos = async () => {
|
||||
};
|
||||
|
||||
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...");
|
||||
return;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,18 +7,18 @@
|
||||
"featured": "مميز",
|
||||
"surprise_me": "مفاجئني",
|
||||
"no_results": "لم يتم العثور على نتائج",
|
||||
"start_typing": "ابدأ الكتابة للبحث...",
|
||||
"hot": "الأكثر شيوعًا الآن",
|
||||
"start_typing": "ابدأ بالكتابة للبحث...",
|
||||
"hot": "الأكثر شهرة الآن",
|
||||
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||
"achievements": "🏆 ألعاب للتغلب عليها"
|
||||
"achievements": "🏆 ألعاب يجب إكمالها"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "الكـتالوج",
|
||||
"catalogue": "الفهرس",
|
||||
"downloads": "التنزيلات",
|
||||
"settings": "الإعدادات",
|
||||
"my_library": "مكتبتي",
|
||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||
"paused": "{{title}} (معلّق)",
|
||||
"downloading_metadata": "{{title}} (جاري تنزيل البيانات الوصفية...)",
|
||||
"paused": "{{title}} (معلق)",
|
||||
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||
"filter": "تصفية المكتبة",
|
||||
"home": "الرئيسية",
|
||||
@@ -26,12 +26,13 @@
|
||||
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
||||
"sign_in": "تسجيل الدخول",
|
||||
"friends": "الأصدقاء",
|
||||
"need_help": "تحتاج مساعدة؟"
|
||||
"need_help": "تحتاج مساعدة؟",
|
||||
"favorites": "المفضلة"
|
||||
},
|
||||
"header": {
|
||||
"search": "ابحث عن الألعاب",
|
||||
"search": "بحث الألعاب",
|
||||
"home": "الرئيسية",
|
||||
"catalogue": "الكـتالوج",
|
||||
"catalogue": "الفهرس",
|
||||
"downloads": "التنزيلات",
|
||||
"search_results": "نتائج البحث",
|
||||
"settings": "الإعدادات",
|
||||
@@ -40,16 +41,16 @@
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية لـ {{title}}...",
|
||||
"downloading": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||
"calculating_eta": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - جاري حساب الوقت المتبقي...",
|
||||
"checking_files": "جارٍ فحص ملفات {{title}}... ({{percentage}} اكتمال)"
|
||||
"downloading_metadata": "جاري تنزيل بيانات {{title}} الوصفية...",
|
||||
"downloading": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||
"calculating_eta": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - جاري حساب الوقت المتبقي...",
|
||||
"checking_files": "جاري فحص ملفات {{title}}... ({{percentage}} مكتمل)"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "تصفية...",
|
||||
"developers": "المطورون",
|
||||
"genres": "الأنواع",
|
||||
"tags": "العلامات",
|
||||
"tags": "الوسوم",
|
||||
"publishers": "الناشرون",
|
||||
"download_sources": "مصادر التنزيل",
|
||||
"result_count": "{{resultCount}} نتيجة",
|
||||
@@ -68,34 +69,34 @@
|
||||
"cancel": "إلغاء",
|
||||
"remove": "إزالة",
|
||||
"space_left_on_disk": "{{space}} متبقي على القرص",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"calculating_eta": "جارٍ حساب الوقت المتبقي...",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"filter": "تصفية الحزم المعاد تعبئتها",
|
||||
"eta": "الانتهاء المتوقع {{eta}}",
|
||||
"calculating_eta": "جاري حساب الوقت المتبقي...",
|
||||
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||
"filter": "تصفية الإصدارات المعادة",
|
||||
"requirements": "متطلبات النظام",
|
||||
"minimum": "الحد الأدنى",
|
||||
"recommended": "مُوصى به",
|
||||
"paused": "معلّق",
|
||||
"recommended": "مستحسن",
|
||||
"paused": "معلق",
|
||||
"release_date": "تاريخ الإصدار {{date}}",
|
||||
"publisher": "نشر بواسطة {{publisher}}",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"amount_hours": "{{amount}} ساعة",
|
||||
"amount_minutes": "{{amount}} دقيقة",
|
||||
"accuracy": "دقة {{accuracy}}%",
|
||||
"add_to_library": "إضافة إلى المكتبة",
|
||||
"remove_from_library": "إزالة من المكتبة",
|
||||
"no_downloads": "لا توجد تنزيلات متاحة",
|
||||
"play_time": "لعب لمدة {{amount}}",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"play_time": "وقت اللعب {{amount}}",
|
||||
"last_time_played": "آخر مرة لعب {{period}}",
|
||||
"not_played_yet": "لم تلعب {{title}} بعد",
|
||||
"next_suggestion": "الاقتراح التالي",
|
||||
"play": "تشغيل",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"deleting": "جاري حذف المثبت...",
|
||||
"close": "إغلاق",
|
||||
"playing_now": "يتم التشغيل الآن",
|
||||
"playing_now": "جاري التشغيل الآن",
|
||||
"change": "تغيير",
|
||||
"repacks_modal_description": "اختر الحزمة المعاد تعبئتها التي تريد تنزيلها",
|
||||
"repacks_modal_description": "اختر الإصدار المعاد الذي تريد تنزيله",
|
||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
||||
"download_now": "تنزيل الآن",
|
||||
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
||||
@@ -110,12 +111,12 @@
|
||||
"select_executable": "تحديد",
|
||||
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
||||
"open_folder": "فتح المجلد",
|
||||
"open_download_location": "عرض الملفات المحملة",
|
||||
"open_download_location": "عرض الملفات المنزلة",
|
||||
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
||||
"clear": "مسح",
|
||||
"remove_files": "إزالة الملفات",
|
||||
"remove_from_library_title": "هل أنت متأكد؟",
|
||||
"remove_from_library_description": "سيؤدي هذا إلى إزالة {{game}} من مكتبتك",
|
||||
"remove_from_library_description": "سيتم إزالة {{game}} من مكتبتك",
|
||||
"options": "خيارات",
|
||||
"executable_section_title": "ملف التشغيل",
|
||||
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||
@@ -123,35 +124,35 @@
|
||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||
"danger_zone_section_title": "منطقة الخطر",
|
||||
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||
"download_in_progress": "تنزيل قيد التقدم",
|
||||
"download_in_progress": "جاري التنزيل",
|
||||
"download_paused": "التنزيل معلق",
|
||||
"last_downloaded_option": "خيار التنزيل الأخير",
|
||||
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
||||
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يناسب جميع الأعمار. هل تريد المتابعة؟",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يكون مناسبًا لجميع الأعمار. هل تريد المتابعة؟",
|
||||
"allow_nsfw_content": "متابعة",
|
||||
"refuse_nsfw_content": "رجوع",
|
||||
"stats": "الإحصائيات",
|
||||
"download_count": "مرات التنزيل",
|
||||
"download_count": "التنزيلات",
|
||||
"player_count": "اللاعبون النشطون",
|
||||
"download_error": "خيار التنزيل هذا غير متاح",
|
||||
"download": "تنزيل",
|
||||
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
||||
"warning": "تحذير:",
|
||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يبقى Hydra مفتوحًا حتى اكتماله. إذا أغلق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يظل Hydra مفتوحًا حتى اكتماله. إذا تم إغلاق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||
"achievements": "الإنجازات",
|
||||
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "حفظ سحابي",
|
||||
"cloud_save_description": "احفظ تقدمك على السحابة واستمر في اللعب من أي جهاز",
|
||||
"cloud_save_description": "احفظ تقدمك في السحابة واستمر في اللعب من أي جهاز",
|
||||
"backups": "النسخ الاحتياطية",
|
||||
"install_backup": "تثبيت",
|
||||
"delete_backup": "حذف",
|
||||
"create_backup": "نسخة احتياطية جديدة",
|
||||
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
||||
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
||||
"restoring_backup": "جارٍ استعادة النسخة الاحتياطية ({{progress}} اكتمال)...",
|
||||
"uploading_backup": "جارٍ رفع النسخة الاحتياطية...",
|
||||
"restoring_backup": "جاري استعادة النسخة الاحتياطية ({{progress}} مكتمل)...",
|
||||
"uploading_backup": "جاري رفع النسخة الاحتياطية...",
|
||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
||||
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
||||
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||
@@ -164,61 +165,67 @@
|
||||
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||
"manage_files": "إدارة الملفات",
|
||||
"loading_save_preview": "جارٍ البحث عن حفظات الألعاب...",
|
||||
"loading_save_preview": "جاري البحث عن حفظات اللعبة...",
|
||||
"wine_prefix": "بادئة Wine",
|
||||
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||
"launch_options": "خيارات التشغيل",
|
||||
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
||||
"launch_options_placeholder": "لم يتم تحديد أي معاملات",
|
||||
"launch_options_placeholder": "لا توجد معلمات محددة",
|
||||
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||
"backup_deletion_failed": "فشل حذف النسخة الاحتياطية",
|
||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
||||
"achievements_not_sync": "تعرف على كيفية مزامنة إنجازاتك",
|
||||
"backup_deletion_failed": "فشل في حذف النسخة الاحتياطية",
|
||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى من النسخ الاحتياطية لهذه اللعبة",
|
||||
"achievements_not_sync": "شاهد كيفية مزامنة إنجازاتك",
|
||||
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||
"select_folder": "حدد المجلد",
|
||||
"backup_from": "نسخة احتياطية من {{date}}",
|
||||
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
||||
"no_directory_selected": "لم يتم تحديد مجلد",
|
||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا لمعرفة المزيد.",
|
||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا للمزيد من المعلومات.",
|
||||
"reset_achievements": "إعادة تعيين الإنجازات",
|
||||
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
||||
"reset_achievements_title": "هل أنت متأكد؟",
|
||||
"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": {
|
||||
"title": "تفعيل Hydra",
|
||||
"installation_id": "معرف التثبيت:",
|
||||
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا يجب أن يكون لديك هذا.",
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فأنت لا يجب أن يكون لديك هذا.",
|
||||
"activate": "تفعيل",
|
||||
"loading": "جارٍ التحميل..."
|
||||
"loading": "جاري التحميل..."
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"paused": "معلّق",
|
||||
"verifying": "جارٍ التحقق...",
|
||||
"eta": "الانتهاء المتوقع {{eta}}",
|
||||
"paused": "معلق",
|
||||
"verifying": "جاري التحقق...",
|
||||
"completed": "مكتمل",
|
||||
"removed": "غير محمل",
|
||||
"removed": "غير منزّل",
|
||||
"cancel": "إلغاء",
|
||||
"filter": "تصفية الألعاب المحملة",
|
||||
"filter": "تصفية الألعاب المنزلة",
|
||||
"remove": "إزالة",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"delete": "إزالة المثبت",
|
||||
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||
"deleting": "جاري حذف المثبت...",
|
||||
"delete": "حذف المثبت",
|
||||
"delete_modal_title": "هل أنت متأكد؟",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
||||
"install": "تثبيت",
|
||||
"download_in_progress": "قيد التقدم",
|
||||
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||
"downloads_completed": "مكتمل",
|
||||
"downloads_completed": "مكتملة",
|
||||
"queued": "في قائمة الانتظار",
|
||||
"no_downloads_title": "فارغ جدًا",
|
||||
"no_downloads_title": "لا شيء هنا",
|
||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
||||
"checking_files": "جارٍ فحص الملفات...",
|
||||
"seeding": "التوزيع",
|
||||
"checking_files": "جاري فحص الملفات...",
|
||||
"seeding": "جاري التوزيع",
|
||||
"stop_seeding": "إيقاف التوزيع",
|
||||
"resume_seeding": "استئناف التوزيع",
|
||||
"options": "إدارة"
|
||||
@@ -228,8 +235,8 @@
|
||||
"change": "تحديث",
|
||||
"notifications": "الإشعارات",
|
||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة معاد تعبئتها جديدة",
|
||||
"real_debrid_api_token_label": "رمز واجهة برمجة تطبيقات Real-Debrid",
|
||||
"enable_repack_list_notifications": "عند إضافة إصدار معاد جديد",
|
||||
"real_debrid_api_token_label": "رمز Real-Debrid API",
|
||||
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
||||
"launch_with_system": "تشغيل Hydra مع بدء النظام",
|
||||
"general": "عام",
|
||||
@@ -238,14 +245,14 @@
|
||||
"language": "اللغة",
|
||||
"api_token": "رمز API",
|
||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، محدودة فقط بسرعة اتصالك بالإنترنت.",
|
||||
"debrid_invalid_token": "رمز API غير صالح",
|
||||
"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}}\"",
|
||||
"save_changes": "حفظ التغييرات",
|
||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL للمصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||
"validate_download_source": "تحقق",
|
||||
"remove_download_source": "إزالة",
|
||||
"removed_download_sources": "تمت إزالة مصادر التنزيل",
|
||||
@@ -258,7 +265,7 @@
|
||||
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
||||
"download_source_url": "عنوان URL لمصدر التنزيل",
|
||||
"download_source_url": "عنوان مصدر التنزيل",
|
||||
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||
"download_source_up_to_date": "محدث",
|
||||
"download_source_errored": "خطأ",
|
||||
@@ -278,13 +285,13 @@
|
||||
"profile_visibility": "رؤية الملف الشخصي",
|
||||
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"source_already_exists": "تمت إضافة هذا المصدر مسبقًا",
|
||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
||||
"source_already_exists": "هذا المصدر مضاف مسبقًا",
|
||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالح",
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||
"enable_achievement_notifications": "عند فتح إنجاز",
|
||||
"launch_minimized": "تشغيل Hydra مصغرًا",
|
||||
"disable_nsfw_alert": "تعطيل تنبيه المحتوى غير اللائق",
|
||||
"disable_nsfw_alert": "تعطيل تنبيهات المحتوى غير اللائق",
|
||||
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
||||
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
||||
"account": "الحساب",
|
||||
@@ -302,18 +309,47 @@
|
||||
"become_subscriber": "كن مشتركًا في Hydra Cloud",
|
||||
"subscription_renew_cancelled": "تم تعطيل التجديد التلقائي",
|
||||
"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": {
|
||||
"download_complete": "اكتمل التنزيل",
|
||||
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||
"repack_list_updated": "تم تحديث قائمة الحزم المعاد تعبئتها",
|
||||
"repack_count_one": "تمت إضافة {{count}} حزمة معاد تعبئتها",
|
||||
"repack_count_other": "تمت إضافة {{count}} حزم معاد تعبئتها",
|
||||
"repack_list_updated": "تم تحديث قائمة الإصدارات المعادة",
|
||||
"repack_count_one": "تمت إضافة {{count}} إصدار معاد",
|
||||
"repack_count_other": "تمت إضافة {{count}} إصدارات معادة",
|
||||
"new_update_available": "الإصدار {{version}} متوفر",
|
||||
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
||||
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} آخرين تم فتحهم"
|
||||
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} أخرى تم فتحها"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "فتح Hydra",
|
||||
@@ -325,7 +361,7 @@
|
||||
"binary_not_found_modal": {
|
||||
"title": "البرامج غير مثبتة",
|
||||
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة لينكس الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||
},
|
||||
"modal": {
|
||||
"close": "زر الإغلاق"
|
||||
@@ -334,16 +370,16 @@
|
||||
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"amount_hours": "{{amount}} ساعة",
|
||||
"amount_minutes": "{{amount}} دقيقة",
|
||||
"last_time_played": "آخر مرة لعب {{period}}",
|
||||
"activity": "النشاط الأخير",
|
||||
"library": "المكتبة",
|
||||
"total_play_time": "إجمالي وقت اللعب",
|
||||
"no_recent_activity_title": "همم... لا شيء هنا",
|
||||
"no_recent_activity_title": "لا شيء هنا...",
|
||||
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
||||
"display_name": "اسم العرض",
|
||||
"saving": "جارٍ الحفظ",
|
||||
"saving": "جاري الحفظ",
|
||||
"save": "حفظ",
|
||||
"edit_profile": "تعديل الملف الشخصي",
|
||||
"saved_successfully": "تم الحفظ بنجاح",
|
||||
@@ -352,13 +388,13 @@
|
||||
"cancel": "إلغاء",
|
||||
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||
"sign_out": "تسجيل الخروج",
|
||||
"playing_for": "يلعب لمدة {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية بعد الآن، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||
"playing_for": "جاري اللعب لمدة {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||
"add_friends": "إضافة أصدقاء",
|
||||
"add": "إضافة",
|
||||
"friend_code": "رمز الصديق",
|
||||
"see_profile": "عرض الملف الشخصي",
|
||||
"sending": "جارٍ الإرسال",
|
||||
"sending": "جاري الإرسال",
|
||||
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||
"friends": "الأصدقاء",
|
||||
"friends_list": "قائمة الأصدقاء",
|
||||
@@ -377,19 +413,19 @@
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"unblock": "إلغاء الحظر",
|
||||
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
||||
"pending": "قيد الانتظار",
|
||||
"pending": "معلق",
|
||||
"no_pending_invites": "ليس لديك دعوات معلقة",
|
||||
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
||||
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
||||
"locked_profile": "هذا الملف الشخصي خاص",
|
||||
"image_process_failure": "فشل معالجة الصورة",
|
||||
"image_process_failure": "فشل في معالجة الصورة",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
||||
"displayname_max_length": "يجب ألا يتجاوز اسم العرض 50 حرفًا",
|
||||
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||
"displayname_max_length": "يجب أن لا يتجاوز اسم العرض 50 حرفًا",
|
||||
"report_profile": "الإبلاغ عن هذا الملف",
|
||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف؟",
|
||||
"report_description": "معلومات إضافية",
|
||||
"report_description_placeholder": "معلومات إضافية",
|
||||
"report": "الإبلاغ",
|
||||
@@ -399,32 +435,32 @@
|
||||
"report_reason_spam": "بريد عشوائي",
|
||||
"report_reason_other": "أخرى",
|
||||
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||
"your_friend_code": "رمز صديقك:",
|
||||
"upload_banner": "تحميل بانر",
|
||||
"uploading_banner": "جارٍ تحميل البانر...",
|
||||
"your_friend_code": "رمز الصديق الخاص بك:",
|
||||
"upload_banner": "رفع بانر",
|
||||
"uploading_banner": "جاري رفع البانر...",
|
||||
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||
"stats": "الإحصائيات",
|
||||
"achievements": "إنجازات",
|
||||
"achievements": "الإنجازات",
|
||||
"games": "الألعاب",
|
||||
"top_percentile": "ال{{percentile}}% الأعلى",
|
||||
"top_percentile": "الأعلى {{percentile}}%",
|
||||
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
||||
"playing": "يلعب {{game}}",
|
||||
"playing": "جاري لعب {{game}}",
|
||||
"achievements_unlocked": "الإنجازات المفتوحة",
|
||||
"earned_points": "النقاط المكتسبة",
|
||||
"show_achievements_on_profile": "عرض إنجازاتك على ملفك الشخصي",
|
||||
"show_points_on_profile": "عرض نقاطك المكتسبة على ملفك الشخصي"
|
||||
"show_achievements_on_profile": "عرض إنجازاتك في ملفك الشخصي",
|
||||
"show_points_on_profile": "عرض نقاطك المكتسبة في ملفك الشخصي"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "تم فتح الإنجاز",
|
||||
"user_achievements": "إنجازات {{displayName}}",
|
||||
"your_achievements": "إنجازاتك",
|
||||
"unlocked_at": "تم الفتح في: {{date}}",
|
||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لعرض هذا المحتوى",
|
||||
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
||||
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
||||
"achievement_earn_points": "اكسب {{points}} نقطة مع هذا الإنجاز",
|
||||
"achievement_earn_points": "احصل على {{points}} نقاط مع هذا الإنجاز",
|
||||
"earned_points": "النقاط المكتسبة:",
|
||||
"available_points": "النقاط المتاحة:",
|
||||
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
||||
@@ -434,10 +470,10 @@
|
||||
"subscribe_now": "اشترك الآن",
|
||||
"cloud_saving": "حفظ سحابي",
|
||||
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||
"animated_profile_picture": "صورة ملف شخصي متحركة",
|
||||
"animated_profile_picture": "صورة ملف متحركة",
|
||||
"premium_support": "دعم ممتاز",
|
||||
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
||||
"animated_profile_banner": "بانر ملف شخصي متحرك",
|
||||
"animated_profile_banner": "بانر ملف متحرك",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
||||
"learn_more": "معرفة المزيد"
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
"paused": "{{title}} (Спынена)",
|
||||
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
||||
"filter": "Фільтар бібліятэкі",
|
||||
"home": "Галоўная"
|
||||
"home": "Галоўная",
|
||||
"favorites": "Улюбленыя"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
"home": "Галоўная",
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"game_has_no_executable": "Играта няма избран изпълним файл",
|
||||
"sign_in": "Вписване",
|
||||
"friends": "Приятели",
|
||||
"need_help": "Имате нужда от помощ??"
|
||||
"need_help": "Имате нужда от помощ??",
|
||||
"favorites": "Любими игри"
|
||||
},
|
||||
"header": {
|
||||
"search": "Търсене",
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
"home": "Inici",
|
||||
"queued": "{{title}} (En espera)",
|
||||
"game_has_no_executable": "El joc encara no té un executable seleccionat",
|
||||
"sign_in": "Entra"
|
||||
"sign_in": "Entra",
|
||||
"favorites": "Favorits"
|
||||
},
|
||||
"header": {
|
||||
"search": "Cerca jocs",
|
||||
|
||||
"home": "Inici",
|
||||
"catalogue": "Catàleg",
|
||||
"downloads": "Baixades",
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
|
||||
"sign_in": "Přihlásit se",
|
||||
"friends": "Přátelé",
|
||||
"need_help": "Potřebujete pomoc?"
|
||||
"need_help": "Potřebujete pomoc?",
|
||||
"favorites": "Oblíbené"
|
||||
},
|
||||
"header": {
|
||||
"search": "Vyhledat hry",
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
"queued": "{{title}} (I køen)",
|
||||
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
|
||||
"sign_in": "Log ind",
|
||||
"friends": "Venner"
|
||||
"friends": "Venner",
|
||||
"favorites": "Favoritter"
|
||||
},
|
||||
"header": {
|
||||
"search": "Søg efter spil",
|
||||
|
||||
"home": "Hjem",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
"home": "Home",
|
||||
"queued": "{{title}} (In Warteschlange)",
|
||||
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
||||
"sign_in": "Anmelden"
|
||||
"sign_in": "Anmelden",
|
||||
"favorites": "Favoriten"
|
||||
},
|
||||
"header": {
|
||||
"search": "Spiele suchen",
|
||||
|
||||
"home": "Home",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"game_has_no_executable": "Game has no executable selected",
|
||||
"sign_in": "Sign in",
|
||||
"friends": "Friends",
|
||||
"need_help": "Need help?"
|
||||
"need_help": "Need help?",
|
||||
"favorites": "Favorites"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
@@ -177,6 +178,8 @@
|
||||
"manage_files_description": "Manage which files will be backed up and restored",
|
||||
"select_folder": "Select folder",
|
||||
"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",
|
||||
"no_directory_selected": "No directory selected",
|
||||
"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_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
|
||||
"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": {
|
||||
"title": "Activate Hydra",
|
||||
@@ -307,10 +312,35 @@
|
||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||
"bill_sent_until": "Your next bill will be sent until this day",
|
||||
"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",
|
||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||
"torbox_account_linked": "TorBox account linked",
|
||||
"real_debrid_account_linked": "Real-Debrid account linked"
|
||||
"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": {
|
||||
"download_complete": "Download complete",
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"friends": "Amigos",
|
||||
"need_help": "¿Necesitas ayuda?"
|
||||
"need_help": "¿Necesitas ayuda?",
|
||||
"favorites": "Favoritos"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar juegos",
|
||||
@@ -173,6 +174,8 @@
|
||||
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
||||
"select_folder": "Seleccionar carpeta",
|
||||
"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",
|
||||
"clear": "Limpiar",
|
||||
"no_directory_selected": "No se seleccionó un directorio",
|
||||
@@ -184,7 +187,13 @@
|
||||
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||
"reset_achievements_title": "¿Estás seguro?",
|
||||
"reset_achievements_success": "Logros reiniciados exitosamente",
|
||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
|
||||
"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": {
|
||||
"title": "Activar Hydra",
|
||||
@@ -302,7 +311,37 @@
|
||||
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||
"update_email": "Actualizar correo",
|
||||
"update_password": "Actualizar contraseña"
|
||||
"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": {
|
||||
"download_complete": "Descarga completada",
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"queued": "{{title}} (Järjekorras)",
|
||||
"game_has_no_executable": "Mängul pole käivitusfaili valitud",
|
||||
"sign_in": "Logi sisse",
|
||||
"friends": "Sõbrad"
|
||||
"friends": "Sõbrad",
|
||||
"favorites": "Lemmikud"
|
||||
},
|
||||
"header": {
|
||||
"search": "Otsi mänge",
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
"paused": "{{title}} (متوقف شده)",
|
||||
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
||||
"filter": "فیلتر کردن کتابخانه",
|
||||
"home": "خانه"
|
||||
"home": "خانه",
|
||||
"favorites": "علاقهمندیها"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "جستجوی بازیها",
|
||||
"home": "خانه",
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
"paused": "{{title}} (En pause)",
|
||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||
"filter": "Filtrer la bibliothèque",
|
||||
"home": "Page d’accueil"
|
||||
"home": "Page d’accueil",
|
||||
"favorites": "Favoris"
|
||||
},
|
||||
"header": {
|
||||
"search": "Recherche",
|
||||
|
||||
"catalogue": "Catalogue",
|
||||
"downloads": "Téléchargements",
|
||||
"search_results": "Résultats de la recherche",
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
"paused": "{{title}} (Szünet)",
|
||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||
"filter": "Könyvtár szűrése",
|
||||
"home": "Főoldal"
|
||||
"home": "Főoldal",
|
||||
"favorites": "Kedvenc játékok"
|
||||
},
|
||||
"header": {
|
||||
"search": "Keresés",
|
||||
|
||||
"home": "Főoldal",
|
||||
"catalogue": "Katalógus",
|
||||
"downloads": "Letöltések",
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
"home": "Beranda",
|
||||
"queued": "{{title}} (Antrian)",
|
||||
"game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
|
||||
"sign_in": "Masuk"
|
||||
"sign_in": "Masuk",
|
||||
"favorites": "Favorit"
|
||||
},
|
||||
"header": {
|
||||
"search": "Cari game",
|
||||
|
||||
"home": "Beranda",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Unduhan",
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
"paused": "{{title}} (In pausa)",
|
||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||
"filter": "Filtra libreria",
|
||||
"home": "Home"
|
||||
"home": "Home",
|
||||
"favorites": "Preferiti"
|
||||
},
|
||||
"header": {
|
||||
"search": "Cerca",
|
||||
|
||||
"home": "Home",
|
||||
"catalogue": "Catalogo",
|
||||
"downloads": "Download",
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
"home": "Басты бет",
|
||||
"queued": "{{title}} (Кезекте)",
|
||||
"game_has_no_executable": "Ойынды іске қосу файлы таңдалмаған",
|
||||
"sign_in": "Кіру"
|
||||
"sign_in": "Кіру",
|
||||
"favorites": "Таңдаулылар"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "Іздеу",
|
||||
"home": "Басты бет",
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
"paused": "{{title}} (일시 정지됨)",
|
||||
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
||||
"filter": "라이브러리 정렬",
|
||||
"home": "홈"
|
||||
"home": "홈",
|
||||
"favorites": "즐겨찾기"
|
||||
},
|
||||
|
||||
"header": {
|
||||
"search": "게임 검색하기",
|
||||
"home": "홈",
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
"queued": "{{title}} (I køen)",
|
||||
"game_has_no_executable": "Spillet har ikke noen kjørbar fil valgt",
|
||||
"sign_in": "Logge inn",
|
||||
"friends": "Venner"
|
||||
"friends": "Venner",
|
||||
"favorites": "Favoritter"
|
||||
},
|
||||
"header": {
|
||||
"search": "Søk efter spill",
|
||||
|
||||
"home": "Hjem",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Nedlastinger",
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
"paused": "{{title}} (Gepauzeerd)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filter Bibliotheek",
|
||||
"home": "Home"
|
||||
"home": "Home",
|
||||
"favorites": "Favorieten"
|
||||
},
|
||||
"header": {
|
||||
"search": "Zoek spellen",
|
||||
|
||||
"home": "Home",
|
||||
"catalogue": "Bibliotheek",
|
||||
"downloads": "Downloads",
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
"paused": "{{title}} (Zatrzymano)",
|
||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||
"filter": "Filtruj biblioteke",
|
||||
"home": "Główna"
|
||||
"home": "Główna",
|
||||
"favorites": "Ulubione"
|
||||
},
|
||||
"header": {
|
||||
"search": "Szukaj",
|
||||
|
||||
"home": "Główna",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Pobrane",
|
||||
|
||||
@@ -26,10 +26,12 @@
|
||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||
"sign_in": "Login",
|
||||
"friends": "Amigos",
|
||||
"need_help": "Precisa de ajuda?"
|
||||
"need_help": "Precisa de ajuda?",
|
||||
"favorites": "Favoritos"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Downloads",
|
||||
"search_results": "Resultados da busca",
|
||||
@@ -163,6 +165,8 @@
|
||||
"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",
|
||||
"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",
|
||||
"select_folder": "Selecione a pasta",
|
||||
"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_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
||||
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
|
||||
"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": {
|
||||
"title": "Ativação",
|
||||
@@ -296,10 +302,33 @@
|
||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
||||
"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",
|
||||
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||
"real_debrid_account_linked": "Conta Real-Debrid associada"
|
||||
"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": {
|
||||
"download_complete": "Download concluído",
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
"queued": "{{title}} (Na fila)",
|
||||
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
||||
"sign_in": "Iniciar sessão",
|
||||
"friends": "Amigos"
|
||||
"friends": "Amigos",
|
||||
"favorites": "Favoritos"
|
||||
},
|
||||
"header": {
|
||||
"search": "Procurar jogos",
|
||||
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Transferências",
|
||||
"search_results": "Resultados da pesquisa",
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
"paused": "{{title}} (Pauzat)",
|
||||
"downloading": "{{title}} ({{percentage}} - Se descarcă...)",
|
||||
"filter": "Filtrează biblioteca",
|
||||
"home": "Acasă"
|
||||
"home": "Acasă",
|
||||
"favorites": "Favorite"
|
||||
},
|
||||
"header": {
|
||||
"search": "Caută jocuri",
|
||||
|
||||
"home": "Acasă",
|
||||
"catalogue": "Catalog",
|
||||
"downloads": "Descărcări",
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"game_has_no_executable": "Файл запуска игры не выбран",
|
||||
"sign_in": "Войти",
|
||||
"friends": "Друзья",
|
||||
"need_help": "Нужна помощь?"
|
||||
"need_help": "Нужна помощь?",
|
||||
"favorites": "Избранное"
|
||||
},
|
||||
"header": {
|
||||
"search": "Поиск",
|
||||
@@ -177,12 +178,20 @@
|
||||
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
|
||||
"select_folder": "Выбрать папку",
|
||||
"backup_from": "Резервная копия от {{date}}",
|
||||
"automatic_backup_from": "Автоматическая резервная копия от {{date}}",
|
||||
"enable_automatic_cloud_sync": "Включить автоматическую синхронизацию в облаке",
|
||||
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
||||
"no_directory_selected": "Не выбран каталог",
|
||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||
"reset_achievements_title": "Вы уверены?",
|
||||
"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": {
|
||||
"title": "Активировать Hydra",
|
||||
@@ -300,7 +309,36 @@
|
||||
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
||||
"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": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
|
||||
"sign_in": "Giriş yap",
|
||||
"friends": "Arkadaşlar",
|
||||
"need_help": "Yardıma mı ihtiyacınız var?"
|
||||
"need_help": "Yardıma mı ihtiyacınız var?",
|
||||
"favorites": "Favoriler"
|
||||
},
|
||||
"header": {
|
||||
"search": "Oyunları ara",
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
"home": "Головна",
|
||||
"game_has_no_executable": "Не було вибрано файл для запуску гри",
|
||||
"queued": "{{title}} в черзі",
|
||||
"sign_in": "Увійти"
|
||||
"sign_in": "Увійти",
|
||||
"favorites": "Улюблені"
|
||||
},
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
|
||||
"home": "Головна",
|
||||
"catalogue": "Каталог",
|
||||
"downloads": "Завантаження",
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"queued": "{{title}} (已加入下载队列)",
|
||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||
"sign_in": "登入",
|
||||
"friends": "好友"
|
||||
"friends": "好友",
|
||||
"favorites": "收藏"
|
||||
},
|
||||
"header": {
|
||||
"search": "搜索游戏",
|
||||
|
||||
@@ -3,7 +3,6 @@ import jwt from "jsonwebtoken";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
import { Crypto } from "@main/services";
|
||||
|
||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||
@@ -11,9 +10,7 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
});
|
||||
|
||||
if (!auth) return null;
|
||||
const payload = jwt.decode(
|
||||
Crypto.decrypt(auth.accessToken)
|
||||
) as jwt.JwtPayload;
|
||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||
|
||||
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 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 axios from "axios";
|
||||
import os from "node:os";
|
||||
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;
|
||||
};
|
||||
import { t } from "i18next";
|
||||
import { format } from "date-fns";
|
||||
|
||||
const uploadSaveGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -46,61 +10,15 @@ const uploadSaveGame = async (
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null
|
||||
) => {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const bundleLocation = await bundleBackup(
|
||||
shop,
|
||||
return CloudSync.uploadSaveGame(
|
||||
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);
|
||||
|
||||
@@ -13,6 +13,8 @@ import "./catalogue/get-developers";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./hardware/check-folder-write-permission";
|
||||
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/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
@@ -29,11 +31,13 @@ import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./library/select-game-wine-prefix";
|
||||
import "./library/reset-game-achievements";
|
||||
import "./library/toggle-automatic-cloud-sync";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
import "./misc/get-features";
|
||||
import "./misc/show-item-in-folder";
|
||||
import "./misc/get-badges";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./torrenting/resume-game-download";
|
||||
@@ -56,6 +60,7 @@ import "./user/get-blocked-users";
|
||||
import "./user/block-user";
|
||||
import "./user/unblock-user";
|
||||
import "./user/get-user-friends";
|
||||
import "./user/get-auth";
|
||||
import "./user/get-user-stats";
|
||||
import "./user/report-user";
|
||||
import "./user/get-unlocked-achievements";
|
||||
@@ -75,6 +80,16 @@ import "./cloud-save/upload-save-game";
|
||||
import "./cloud-save/delete-game-artifact";
|
||||
import "./cloud-save/select-game-backup-path";
|
||||
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";
|
||||
|
||||
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 { createGame } from "@main/services/library-sync";
|
||||
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";
|
||||
|
||||
const addGameToLibrary = async (
|
||||
@@ -46,9 +46,9 @@ const addGameToLibrary = async (
|
||||
|
||||
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 { registerEvent } from "../register-event";
|
||||
import { Crypto, HydraApi } from "@main/services";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
|
||||
@@ -14,7 +14,7 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
}
|
||||
|
||||
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||
refreshToken: Crypto.decrypt(auth.refreshToken),
|
||||
refreshToken: auth.refreshToken,
|
||||
}).then((response) => response.accessToken);
|
||||
|
||||
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 downloadsSublevel.del(downloadKey);
|
||||
const download = await downloadsSublevel.get(downloadKey);
|
||||
|
||||
if (!download) return;
|
||||
|
||||
await downloadsSublevel.put(downloadKey, {
|
||||
...download,
|
||||
status: "removed",
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||
|
||||
@@ -15,6 +15,7 @@ const pauseGameSeed = async (
|
||||
|
||||
await downloadsSublevel.put(downloadKey, {
|
||||
...download,
|
||||
status: "complete",
|
||||
shouldSeed: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ const resumeGameSeed = async (
|
||||
shop: GameShop,
|
||||
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;
|
||||
|
||||
await downloadsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
await downloadsSublevel.put(downloadKey, {
|
||||
...download,
|
||||
status: "seeding",
|
||||
shouldSeed: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { Crypto } from "@main/services";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
const getUserPreferences = async () =>
|
||||
db
|
||||
.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
})
|
||||
.then((userPreferences) => {
|
||||
if (userPreferences?.realDebridApiToken) {
|
||||
userPreferences.realDebridApiToken = Crypto.decrypt(
|
||||
userPreferences.realDebridApiToken
|
||||
);
|
||||
}
|
||||
|
||||
if (userPreferences?.torBoxApiToken) {
|
||||
userPreferences.torBoxApiToken = Crypto.decrypt(
|
||||
userPreferences.torBoxApiToken
|
||||
);
|
||||
}
|
||||
|
||||
return userPreferences;
|
||||
});
|
||||
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
registerEvent("getUserPreferences", getUserPreferences);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { registerEvent } from "../register-event";
|
||||
import type { UserPreferences } from "@types";
|
||||
import i18next from "i18next";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { Crypto } from "@main/services";
|
||||
import { patchUserProfile } from "../profile/update-profile";
|
||||
|
||||
const updateUserPreferences = async (
|
||||
@@ -24,16 +23,6 @@ const updateUserPreferences = async (
|
||||
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) {
|
||||
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
|
||||
.map((achievementData) => {
|
||||
const unlockedAchiementData = unlockedAchievements.find(
|
||||
const unlockedAchievementData = unlockedAchievements.find(
|
||||
(localAchievement) => {
|
||||
return (
|
||||
localAchievement.name.toUpperCase() ==
|
||||
@@ -45,11 +45,11 @@ export const getUnlockedAchievements = async (
|
||||
? achievementData.icon
|
||||
: achievementData.icongray;
|
||||
|
||||
if (unlockedAchiementData) {
|
||||
if (unlockedAchievementData) {
|
||||
return {
|
||||
...achievementData,
|
||||
unlocked: true,
|
||||
unlockTime: unlockedAchiementData.unlockTime,
|
||||
unlockTime: unlockedAchievementData.unlockTime,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import kill from "kill-port";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import resources from "@locales";
|
||||
@@ -58,7 +59,7 @@ app.whenReady().then(async () => {
|
||||
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, {
|
||||
valueEncoding: "utf-8",
|
||||
@@ -85,6 +86,29 @@ const handleDeepLinkPath = (uri?: string) => {
|
||||
|
||||
if (url.host === "install-source") {
|
||||
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) {
|
||||
logger.error("Error handling deep link", uri, error);
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./games";
|
||||
export * from "./game-shop-cache";
|
||||
export * from "./game-achievements";
|
||||
export * from "./keys";
|
||||
export * from "./themes";
|
||||
|
||||
@@ -5,6 +5,7 @@ export const levelKeys = {
|
||||
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
||||
user: "user",
|
||||
auth: "auth",
|
||||
themes: "themes",
|
||||
gameShopCache: "gameShopCache",
|
||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||
`${shop}:${objectId}:${language}`,
|
||||
@@ -13,4 +14,5 @@ export const levelKeys = {
|
||||
userPreferences: "userPreferences",
|
||||
language: "language",
|
||||
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 {
|
||||
Crypto,
|
||||
DownloadManager,
|
||||
logger,
|
||||
Ludusavi,
|
||||
startMainLoop,
|
||||
} from "./services";
|
||||
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||
import { RealDebridClient } from "./services/download/real-debrid";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
@@ -38,13 +32,11 @@ export const loadState = async () => {
|
||||
Aria2.spawn();
|
||||
|
||||
if (userPreferences?.realDebridApiToken) {
|
||||
RealDebridClient.authorize(
|
||||
Crypto.decrypt(userPreferences.realDebridApiToken)
|
||||
);
|
||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||
}
|
||||
|
||||
if (userPreferences?.torBoxApiToken) {
|
||||
TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken));
|
||||
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||
}
|
||||
|
||||
Ludusavi.addManifestToLudusaviConfig();
|
||||
@@ -57,21 +49,17 @@ export const loadState = async () => {
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => {
|
||||
return sortBy(
|
||||
games.filter((game) => game.queued),
|
||||
"timestamp",
|
||||
"DESC"
|
||||
);
|
||||
return sortBy(games, "timestamp", "DESC");
|
||||
});
|
||||
|
||||
const [nextItemOnQueue] = downloads;
|
||||
const [nextItemOnQueue] = downloads.filter((game) => game.queued);
|
||||
|
||||
const downloadsToSeed = downloads.filter(
|
||||
(download) =>
|
||||
download.shouldSeed &&
|
||||
download.downloader === Downloader.Torrent &&
|
||||
download.progress === 1 &&
|
||||
download.uri !== null
|
||||
(game) =>
|
||||
game.shouldSeed &&
|
||||
game.downloader === Downloader.Torrent &&
|
||||
game.progress === 1 &&
|
||||
game.uri !== null
|
||||
);
|
||||
|
||||
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||
@@ -123,9 +111,7 @@ const migrateFromSqlite = async () => {
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
...rest,
|
||||
realDebridApiToken: realDebridApiToken
|
||||
? Crypto.encrypt(realDebridApiToken)
|
||||
: null,
|
||||
realDebridApiToken,
|
||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||
runAtStartup: rest.runAtStartup === 1,
|
||||
startMinimized: rest.startMinimized === 1,
|
||||
@@ -144,7 +130,9 @@ const migrateFromSqlite = async () => {
|
||||
);
|
||||
|
||||
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>(
|
||||
levelKeys.auth,
|
||||
{
|
||||
accessToken: Crypto.encrypt(users[0].accessToken),
|
||||
refreshToken: Crypto.encrypt(users[0].refreshToken),
|
||||
accessToken: users[0].accessToken,
|
||||
refreshToken: users[0].refreshToken,
|
||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,23 +23,21 @@ const saveAchievementsOnLocal = async (
|
||||
return gameAchievementsSublevel
|
||||
.get(levelKey)
|
||||
.then(async (gameAchievement) => {
|
||||
if (gameAchievement) {
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
...gameAchievement,
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
});
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
achievements: gameAchievement?.achievements ?? [],
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
});
|
||||
|
||||
if (!sendUpdateEvent) return;
|
||||
if (!sendUpdateEvent) return;
|
||||
|
||||
return getUnlockedAchievements(objectId, shop, true)
|
||||
.then((achievements) => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
achievements
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
return getUnlockedAchievements(objectId, shop, true)
|
||||
.then((achievements) => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
achievements
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,7 +131,7 @@ export const mergeAchievements = async (
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err! instanceof SubscriptionRequiredError) {
|
||||
if (err instanceof SubscriptionRequiredError) {
|
||||
achievementsLogger.log(
|
||||
"Achievements not synchronized on API due to lack of subscription",
|
||||
game.objectId,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parseAchievementFile } from "./parse-achievement-file";
|
||||
import { mergeAchievements } from "./merge-achievements";
|
||||
import type { Game, UnlockedAchievement } from "@types";
|
||||
|
||||
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
||||
export const updateLocalUnlockedAchievements = async (game: Game) => {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
|
||||
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 { publishDownloadCompleteNotification } from "../notifications";
|
||||
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 {
|
||||
LibtorrentPayload,
|
||||
@@ -219,8 +225,10 @@ export class DownloadManager {
|
||||
} as PauseDownloadPayload)
|
||||
.catch(() => {});
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
this.downloadingGameId = null;
|
||||
if (downloadKey === this.downloadingGameId) {
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeDownload(download: Download) {
|
||||
@@ -228,14 +236,17 @@ export class DownloadManager {
|
||||
}
|
||||
|
||||
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
||||
await PythonRPC.rpc.post("/action", {
|
||||
action: "cancel",
|
||||
game_id: downloadKey,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
await PythonRPC.rpc
|
||||
.post("/action", {
|
||||
action: "cancel",
|
||||
game_id: downloadKey,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to cancel game download", err);
|
||||
});
|
||||
|
||||
if (downloadKey === this.downloadingGameId) {
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
@@ -278,11 +289,12 @@ export class DownloadManager {
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = download.uri.split("/").pop();
|
||||
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: downloadId,
|
||||
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
|
||||
url: downloadUrl,
|
||||
save_path: download.downloadPath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
TorBoxAddTorrentRequest,
|
||||
TorBoxRequestLinkRequest,
|
||||
} from "@types";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
export class TorBoxClient {
|
||||
private static instance: AxiosInstance;
|
||||
@@ -18,6 +19,7 @@ export class TorBoxClient {
|
||||
baseURL: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"User-Agent": `Hydra/${appVersion}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,47 +1,71 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { wrapper } from "axios-cookiejar-support";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
|
||||
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> {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/");
|
||||
try {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
|
||||
const fileCode = pathSegments[0];
|
||||
|
||||
const fileCode = decodeURIComponent(pathSegments[1]);
|
||||
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
|
||||
await this.jar.setCookie("lang=english;", "https://datanodes.to");
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
rand: "",
|
||||
referer: "https://datanodes.to/download",
|
||||
method_free: "Free Download >>",
|
||||
method_premium: "",
|
||||
adblock_detected: "",
|
||||
});
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
method_free: "Free Download >>",
|
||||
dl: "1",
|
||||
});
|
||||
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
|
||||
Host: "datanodes.to",
|
||||
Origin: "https://datanodes.to",
|
||||
Referer: "https://datanodes.to/download",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||
Referer: "https://datanodes.to/download",
|
||||
Origin: "https://datanodes.to",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
if (typeof response.data === "object" && response.data.url) {
|
||||
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 "./datanodes";
|
||||
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 { levelKeys } from "@main/level/sublevels";
|
||||
import type { Auth, User } from "@types";
|
||||
import { Crypto } from "./crypto";
|
||||
|
||||
interface HydraApiOptions {
|
||||
needsAuth?: boolean;
|
||||
@@ -32,8 +31,9 @@ export class HydraApi {
|
||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||
|
||||
private static readonly secondsToMilliseconds = (seconds: number) =>
|
||||
seconds * 1000;
|
||||
private static secondsToMilliseconds(seconds: number) {
|
||||
return seconds * 1000;
|
||||
}
|
||||
|
||||
private static userAuth: HydraApiUserAuth = {
|
||||
authToken: "",
|
||||
@@ -81,8 +81,8 @@ export class HydraApi {
|
||||
db.put<string, Auth>(
|
||||
levelKeys.auth,
|
||||
{
|
||||
accessToken: Crypto.encrypt(accessToken),
|
||||
refreshToken: Crypto.encrypt(refreshToken),
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
{ valueEncoding: "json" }
|
||||
@@ -204,12 +204,8 @@ export class HydraApi {
|
||||
const user = result.at(1) as User | undefined;
|
||||
|
||||
this.userAuth = {
|
||||
authToken: userAuth?.accessToken
|
||||
? Crypto.decrypt(userAuth.accessToken)
|
||||
: "",
|
||||
refreshToken: userAuth?.refreshToken
|
||||
? Crypto.decrypt(userAuth.refreshToken)
|
||||
: "",
|
||||
authToken: userAuth?.accessToken ?? "",
|
||||
refreshToken: userAuth?.refreshToken ?? "",
|
||||
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
||||
subscription: user?.subscription
|
||||
? { expiresAt: user.subscription?.expiresAt }
|
||||
@@ -258,7 +254,7 @@ export class HydraApi {
|
||||
levelKeys.auth,
|
||||
{
|
||||
...auth,
|
||||
accessToken: Crypto.encrypt(accessToken),
|
||||
accessToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
{ valueEncoding: "json" }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./crypto";
|
||||
export * from "./logger";
|
||||
export * from "./steam";
|
||||
export * from "./steam-250";
|
||||
@@ -8,3 +7,4 @@ export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
export * from "./hydra-api";
|
||||
export * from "./ludusavi";
|
||||
export * from "./cloud-sync";
|
||||
|
||||
@@ -24,7 +24,7 @@ export const mergeWithRemoteGames = async () => {
|
||||
? game.playTimeInMilliseconds
|
||||
: localGame.playTimeInMilliseconds;
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...localGame,
|
||||
remoteId: game.id,
|
||||
lastTimePlayed: updatedLastTimePlayed,
|
||||
@@ -39,7 +39,7 @@ export const mergeWithRemoteGames = async () => {
|
||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
objectId: game.objectId,
|
||||
title: steamGame?.name,
|
||||
remoteId: game.id,
|
||||
|
||||
@@ -6,6 +6,9 @@ import axios from "axios";
|
||||
import { exec } from "child_process";
|
||||
import { ProcessPayload } from "./download/types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { t } from "i18next";
|
||||
import { CloudSync } from "./cloud-sync";
|
||||
import { format } from "date-fns";
|
||||
|
||||
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 ""}'`,
|
||||
@@ -225,6 +228,18 @@ function onOpenGame(game: Game) {
|
||||
|
||||
if (game.remoteId) {
|
||||
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 {
|
||||
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
||||
}
|
||||
@@ -287,6 +302,18 @@ const onCloseGame = (game: Game) => {
|
||||
performance.now() - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
).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 {
|
||||
createGame(game).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -17,13 +17,37 @@ import { HydraApi } from "./hydra-api";
|
||||
import UserAgent from "user-agents";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { slice, sortBy } from "lodash-es";
|
||||
import type { UserPreferences } from "@types";
|
||||
import type { ScreenState, UserPreferences } from "@types";
|
||||
import { AuthPage } from "@shared";
|
||||
import { isStaging } from "@main/constants";
|
||||
|
||||
export class WindowManager {
|
||||
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 = "") {
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// 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;
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
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: "#151515",
|
||||
height: 34,
|
||||
},
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
const { isMaximized = false, ...configWithoutMaximized } =
|
||||
await this.loadScreenConfig();
|
||||
|
||||
this.updateInitialConfig(configWithoutMaximized);
|
||||
|
||||
this.mainWindow = new BrowserWindow(
|
||||
this.initialConfigInitializationMainWindow
|
||||
);
|
||||
|
||||
if (isMaximized) {
|
||||
this.mainWindow.maximize();
|
||||
}
|
||||
|
||||
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
(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) {
|
||||
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) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.loadMainWindowURL(hash);
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
CatalogueSearchPayload,
|
||||
SeedingStatus,
|
||||
GameAchievement,
|
||||
Theme,
|
||||
} from "@types";
|
||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
@@ -100,6 +101,17 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("putDownloadSource", objectIds),
|
||||
|
||||
/* Library */
|
||||
toggleAutomaticCloudSync: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
automaticCloudSync: boolean
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"toggleAutomaticCloudSync",
|
||||
shop,
|
||||
objectId,
|
||||
automaticCloudSync
|
||||
),
|
||||
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
||||
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
||||
createGameShortcut: (shop: GameShop, objectId: string) =>
|
||||
@@ -110,11 +122,16 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
executablePath: string | null
|
||||
) =>
|
||||
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: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
launchOptions: string | null
|
||||
) => ipcRenderer.invoke("updateLaunchOptions", shop, objectId, launchOptions),
|
||||
|
||||
selectGameWinePrefix: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -260,6 +277,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
showItemInFolder: (path: string) =>
|
||||
ipcRenderer.invoke("showItemInFolder", path),
|
||||
getFeatures: () => ipcRenderer.invoke("getFeatures"),
|
||||
getBadges: () => ipcRenderer.invoke("getBadges"),
|
||||
platform: process.platform,
|
||||
|
||||
/* Auto update */
|
||||
@@ -319,6 +337,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
||||
|
||||
/* Auth */
|
||||
getAuth: () => ipcRenderer.invoke("getAuth"),
|
||||
signOut: () => ipcRenderer.invoke("signOut"),
|
||||
openAuthWindow: (page: AuthPage) =>
|
||||
ipcRenderer.invoke("openAuthWindow", page),
|
||||
@@ -342,4 +361,30 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||
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 { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||
|
||||
import { injectCustomCss } from "./helpers";
|
||||
import "./app.scss";
|
||||
|
||||
export interface AppProps {
|
||||
@@ -233,6 +234,17 @@ export function App() {
|
||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||
}, [updateRepacks]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAndApplyTheme = async () => {
|
||||
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||
|
||||
if (activeTheme?.code) {
|
||||
injectCustomCss(activeTheme.code);
|
||||
}
|
||||
};
|
||||
loadAndApplyTheme();
|
||||
}, []);
|
||||
|
||||
const playAudio = useCallback(() => {
|
||||
const audio = new Audio(achievementSound);
|
||||
audio.volume = 0.2;
|
||||
@@ -249,6 +261,14 @@ export function App() {
|
||||
};
|
||||
}, [playAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
||||
injectCustomCss(cssString);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
dispatch(closeToast());
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
cursor: pointer;
|
||||
color: globals.$muted-color;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&__image {
|
||||
height: 100%;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PersonIcon } from "@primer/octicons-react";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./avatar.scss";
|
||||
|
||||
@@ -14,11 +15,18 @@ export interface AvatarProps
|
||||
src?: string | null;
|
||||
}
|
||||
|
||||
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
|
||||
export function Avatar({ size, alt, src, className, ...props }: AvatarProps) {
|
||||
return (
|
||||
<div className="profile-avatar" style={{ width: size, height: size }}>
|
||||
{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} />
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,10 @@ export interface BackdropProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||
export function Backdrop({
|
||||
isClosing = false,
|
||||
children,
|
||||
}: Readonly<BackdropProps>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("backdrop", {
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Button({
|
||||
theme = "primary",
|
||||
className,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
}: Readonly<ButtonProps>) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface CheckboxFieldProps
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> {
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
}
|
||||
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
width: 100%;
|
||||
@@ -54,6 +55,7 @@
|
||||
display: flex;
|
||||
color: globals.$muted-color;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
@@ -104,13 +106,14 @@
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__section {
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding-bottom: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ import "./sidebar.scss";
|
||||
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import cn from "classnames";
|
||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||
import { SidebarGameItem } from "./sidebar-game-item";
|
||||
|
||||
const SIDEBAR_MIN_WIDTH = 200;
|
||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||
@@ -167,6 +167,10 @@ export function Sidebar() {
|
||||
}
|
||||
};
|
||||
|
||||
const favoriteGames = useMemo(() => {
|
||||
return sortedLibrary.filter((game) => game.favorite);
|
||||
}, [sortedLibrary]);
|
||||
|
||||
return (
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
@@ -206,6 +210,23 @@ export function Sidebar() {
|
||||
</ul>
|
||||
</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">
|
||||
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||
|
||||
@@ -217,39 +238,16 @@ export function Sidebar() {
|
||||
/>
|
||||
|
||||
<ul className="sidebar__menu">
|
||||
{filteredLibrary.map((game) => (
|
||||
<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>
|
||||
))}
|
||||
{filteredLibrary
|
||||
.filter((game) => !game.favorite)
|
||||
.map((game) => (
|
||||
<SidebarGameItem
|
||||
key={game.id}
|
||||
game={game}
|
||||
handleSidebarGameClick={handleSidebarGameClick}
|
||||
getGameTitle={getGameTitle}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
background-color: globals.$dark-background-color;
|
||||
border-radius: 4px;
|
||||
border: solid 1px globals.$border-color;
|
||||
right: 16px;
|
||||
bottom: 26px + globals.$spacing-unit;
|
||||
right: calc(globals.$spacing-unit * 2);
|
||||
// 28px is the height of the bottom panel
|
||||
bottom: calc(28px + globals.$spacing-unit * 2);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export const VERSION_CODENAME = "Spectre";
|
||||
export const VERSION_CODENAME = "Polychrome";
|
||||
|
||||
export const DOWNLOADER_NAME = {
|
||||
[Downloader.RealDebrid]: "Real-Debrid",
|
||||
@@ -14,3 +14,5 @@ export const DOWNLOADER_NAME = {
|
||||
};
|
||||
|
||||
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>;
|
||||
setCurrentCategoryIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
clearSourceUrl: () => void;
|
||||
clearTheme: () => void;
|
||||
sourceUrl: string | null;
|
||||
currentCategoryIndex: number;
|
||||
blockedUsers: UserBlocks["blocks"];
|
||||
fetchBlockedUsers: () => Promise<void>;
|
||||
appearance: {
|
||||
theme: string | null;
|
||||
authorId: string | null;
|
||||
authorName: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export const settingsContext = createContext<SettingsContext>({
|
||||
updateUserPreferences: async () => {},
|
||||
setCurrentCategoryIndex: () => {},
|
||||
clearSourceUrl: () => {},
|
||||
clearTheme: () => {},
|
||||
sourceUrl: null,
|
||||
currentCategoryIndex: 0,
|
||||
blockedUsers: [],
|
||||
fetchBlockedUsers: async () => {},
|
||||
appearance: {
|
||||
theme: null,
|
||||
authorId: null,
|
||||
authorName: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { Provider } = settingsContext;
|
||||
@@ -34,15 +46,26 @@ export interface SettingsContextProviderProps {
|
||||
|
||||
export function SettingsContextProvider({
|
||||
children,
|
||||
}: SettingsContextProviderProps) {
|
||||
}: Readonly<SettingsContextProviderProps>) {
|
||||
const dispatch = useAppDispatch();
|
||||
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 [blockedUsers, setBlockedUsers] = useState<UserBlocks["blocks"]>([]);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const defaultSourceUrl = searchParams.get("urls");
|
||||
const defaultAppearanceTheme = searchParams.get("theme");
|
||||
const defaultAppearanceAuthorId = searchParams.get("authorId");
|
||||
const defaultAppearanceAuthorName = searchParams.get("authorName");
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceUrl) setCurrentCategoryIndex(2);
|
||||
@@ -54,6 +77,36 @@ export function SettingsContextProvider({
|
||||
}
|
||||
}, [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 blockedUsers = await window.electron.getBlockedUsers(12, 0);
|
||||
setBlockedUsers(blockedUsers.blocks);
|
||||
@@ -79,9 +132,11 @@ export function SettingsContextProvider({
|
||||
setCurrentCategoryIndex,
|
||||
clearSourceUrl,
|
||||
fetchBlockedUsers,
|
||||
clearTheme,
|
||||
currentCategoryIndex,
|
||||
sourceUrl,
|
||||
blockedUsers,
|
||||
appearance,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { darkenColor } from "@renderer/helpers";
|
||||
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 { createContext, useCallback, useEffect, useState } from "react";
|
||||
@@ -16,6 +16,7 @@ export interface UserProfileContext {
|
||||
getUserProfile: () => Promise<void>;
|
||||
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
||||
backgroundImage: string;
|
||||
badges: Badge[];
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
||||
@@ -28,6 +29,7 @@ export const userProfileContext = createContext<UserProfileContext>({
|
||||
getUserProfile: async () => {},
|
||||
setSelectedBackgroundImage: () => {},
|
||||
backgroundImage: "",
|
||||
badges: [],
|
||||
});
|
||||
|
||||
const { Provider } = userProfileContext;
|
||||
@@ -41,12 +43,13 @@ export interface UserProfileContextProviderProps {
|
||||
export function UserProfileContextProvider({
|
||||
children,
|
||||
userId,
|
||||
}: UserProfileContextProviderProps) {
|
||||
}: Readonly<UserProfileContextProviderProps>) {
|
||||
const { userDetails } = useAppSelector((state) => state.userDetails);
|
||||
|
||||
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
const [badges, setBadges] = useState<Badge[]>([]);
|
||||
const [heroBackground, setHeroBackground] = useState(
|
||||
DEFAULT_USER_PROFILE_BACKGROUND
|
||||
);
|
||||
@@ -101,12 +104,18 @@ export function UserProfileContextProvider({
|
||||
});
|
||||
}, [navigate, getUserStats, showErrorToast, userId, t]);
|
||||
|
||||
const getBadges = useCallback(async () => {
|
||||
const badges = await window.electron.getBadges();
|
||||
setBadges(badges);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setUserProfile(null);
|
||||
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
|
||||
|
||||
getUserProfile();
|
||||
}, [getUserProfile]);
|
||||
getBadges();
|
||||
}, [getUserProfile, getBadges]);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
@@ -118,6 +127,7 @@ export function UserProfileContextProvider({
|
||||
setSelectedBackgroundImage,
|
||||
backgroundImage: getBackgroundImageUrl(),
|
||||
userStats,
|
||||
badges,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
32
src/renderer/src/declaration.d.ts
vendored
32
src/renderer/src/declaration.d.ts
vendored
@@ -29,6 +29,9 @@ import type {
|
||||
LibraryGame,
|
||||
GameRunning,
|
||||
TorBoxUser,
|
||||
Theme,
|
||||
Badge,
|
||||
Auth,
|
||||
} from "@types";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type disk from "diskusage";
|
||||
@@ -85,6 +88,11 @@ declare global {
|
||||
getDevelopers: () => Promise<string[]>;
|
||||
|
||||
/* Library */
|
||||
toggleAutomaticCloudSync: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
automaticCloudSync: boolean
|
||||
) => Promise<void>;
|
||||
addGameToLibrary: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -96,6 +104,11 @@ declare global {
|
||||
objectId: string,
|
||||
executablePath: string | null
|
||||
) => Promise<void>;
|
||||
addGameToFavorites: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
removeGameFromFavorites: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<void>;
|
||||
updateLaunchOptions: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -211,6 +224,7 @@ declare global {
|
||||
) => Promise<Electron.OpenDialogReturnValue>;
|
||||
showItemInFolder: (path: string) => Promise<void>;
|
||||
getFeatures: () => Promise<string[]>;
|
||||
getBadges: () => Promise<Badge[]>;
|
||||
platform: NodeJS.Platform;
|
||||
|
||||
/* Auto update */
|
||||
@@ -221,6 +235,7 @@ declare global {
|
||||
restartAndInstallUpdate: () => Promise<void>;
|
||||
|
||||
/* Auth */
|
||||
getAuth: () => Promise<Auth | null>;
|
||||
signOut: () => Promise<void>;
|
||||
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||
getSessionHash: () => Promise<string | null>;
|
||||
@@ -274,6 +289,23 @@ declare global {
|
||||
|
||||
/* Notifications */
|
||||
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 {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const toastSlice = createSlice({
|
||||
state.title = action.payload.title;
|
||||
state.message = action.payload.message;
|
||||
state.type = action.payload.type;
|
||||
state.duration = action.payload.duration ?? 5000;
|
||||
state.duration = action.payload.duration ?? 2000;
|
||||
state.visible = true;
|
||||
},
|
||||
closeToast: (state) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
import Color from "color";
|
||||
import { THEME_WEB_STORE_URL } from "./constants";
|
||||
|
||||
export const formatDownloadProgress = (
|
||||
progress?: number,
|
||||
@@ -53,3 +54,36 @@ export const buildGameAchievementPath = (
|
||||
|
||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||
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) => {
|
||||
await window.electron.pauseGameDownload(shop, objectId);
|
||||
await updateLibrary();
|
||||
dispatch(clearDownload());
|
||||
if (lastPacket?.gameId === `${shop}:${objectId}`) dispatch(clearDownload());
|
||||
};
|
||||
|
||||
const resumeDownload = async (shop: GameShop, objectId: string) => {
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
enum Feature {
|
||||
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
|
||||
Torbox = "TORBOX",
|
||||
}
|
||||
|
||||
export function useFeature() {
|
||||
const [features, setFeatures] = useState<string[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getFeatures().then((features) => {
|
||||
localStorage.setItem("features", JSON.stringify(features || []));
|
||||
setFeatures(features || []);
|
||||
});
|
||||
}, []);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import "@fontsource/noto-sans/500.css";
|
||||
import "@fontsource/noto-sans/700.css";
|
||||
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import "react-tooltip/dist/react-tooltip.css";
|
||||
|
||||
import { App } from "./app";
|
||||
|
||||
@@ -18,23 +19,17 @@ import { store } from "./store";
|
||||
|
||||
import resources from "@locales";
|
||||
|
||||
import { SuspenseWrapper } from "./components";
|
||||
import { logger } from "./logger";
|
||||
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 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({
|
||||
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
||||
@@ -79,32 +74,16 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route element={<App />}>
|
||||
<Route path="/" element={<SuspenseWrapper Component={Home} />} />
|
||||
<Route
|
||||
path="/catalogue"
|
||||
element={<SuspenseWrapper Component={Catalogue} />}
|
||||
/>
|
||||
<Route
|
||||
path="/downloads"
|
||||
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 path="/" element={<Home />} />
|
||||
<Route path="/catalogue" element={<Catalogue />} />
|
||||
<Route path="/downloads" element={<Downloads />} />
|
||||
<Route path="/game/:shop/:objectId" element={<GameDetails />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/profile/:userId" element={<Profile />} />
|
||||
<Route path="/achievements" element={<Achievements />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/theme-editor" element={<ThemeEditor />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
|
||||
@@ -31,7 +31,7 @@ $logo-max-width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: globals.$spacing-unit / 2;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
color: globals.$body-color;
|
||||
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