mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge branch 'main' into Feature/Game-Card-Sources
This commit is contained in:
commit
e12c6daa16
96 changed files with 2289 additions and 455 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -1,5 +1,9 @@
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
@ -1,5 +1,9 @@
|
||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -1,5 +1,9 @@
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: main
|
branches: main
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.1.5",
|
"version": "3.2.2",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
@ -36,6 +36,7 @@
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@fontsource/noto-sans": "^5.1.0",
|
"@fontsource/noto-sans": "^5.1.0",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"kill-port": "^2.0.1",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"parse-torrent": "^11.0.17",
|
"parse-torrent": "^11.0.17",
|
||||||
|
|
|
@ -159,6 +159,8 @@ def action():
|
||||||
downloader = downloads.get(game_id)
|
downloader = downloads.get(game_id)
|
||||||
if downloader:
|
if downloader:
|
||||||
downloader.pause_download()
|
downloader.pause_download()
|
||||||
|
|
||||||
|
if downloading_game_id == game_id:
|
||||||
downloading_game_id = -1
|
downloading_game_id = -1
|
||||||
elif action == 'cancel':
|
elif action == 'cancel':
|
||||||
downloader = downloads.get(game_id)
|
downloader = downloads.get(game_id)
|
||||||
|
|
|
@ -107,7 +107,10 @@ const copyAria2Macos = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyAria2 = () => {
|
const copyAria2 = () => {
|
||||||
if (fs.existsSync("aria2")) {
|
const aria2Path =
|
||||||
|
process.platform === "win32" ? "aria2/aria2c.exe" : "aria2/aria2c";
|
||||||
|
|
||||||
|
if (fs.existsSync(aria2Path)) {
|
||||||
console.log("Aria2 already exists, skipping download...");
|
console.log("Aria2 already exists, skipping download...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,18 @@
|
||||||
"featured": "مميز",
|
"featured": "مميز",
|
||||||
"surprise_me": "مفاجئني",
|
"surprise_me": "مفاجئني",
|
||||||
"no_results": "لم يتم العثور على نتائج",
|
"no_results": "لم يتم العثور على نتائج",
|
||||||
"start_typing": "ابدأ الكتابة للبحث...",
|
"start_typing": "ابدأ بالكتابة للبحث...",
|
||||||
"hot": "الأكثر شيوعًا الآن",
|
"hot": "الأكثر شهرة الآن",
|
||||||
"weekly": "📅 أفضل ألعاب الأسبوع",
|
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||||
"achievements": "🏆 ألعاب للتغلب عليها"
|
"achievements": "🏆 ألعاب يجب إكمالها"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "الكـتالوج",
|
"catalogue": "الفهرس",
|
||||||
"downloads": "التنزيلات",
|
"downloads": "التنزيلات",
|
||||||
"settings": "الإعدادات",
|
"settings": "الإعدادات",
|
||||||
"my_library": "مكتبتي",
|
"my_library": "مكتبتي",
|
||||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
"downloading_metadata": "{{title}} (جاري تنزيل البيانات الوصفية...)",
|
||||||
"paused": "{{title}} (معلّق)",
|
"paused": "{{title}} (معلق)",
|
||||||
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||||
"filter": "تصفية المكتبة",
|
"filter": "تصفية المكتبة",
|
||||||
"home": "الرئيسية",
|
"home": "الرئيسية",
|
||||||
|
@ -26,12 +26,13 @@
|
||||||
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
||||||
"sign_in": "تسجيل الدخول",
|
"sign_in": "تسجيل الدخول",
|
||||||
"friends": "الأصدقاء",
|
"friends": "الأصدقاء",
|
||||||
"need_help": "تحتاج مساعدة؟"
|
"need_help": "تحتاج مساعدة؟",
|
||||||
|
"favorites": "المفضلة"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "ابحث عن الألعاب",
|
"search": "بحث الألعاب",
|
||||||
"home": "الرئيسية",
|
"home": "الرئيسية",
|
||||||
"catalogue": "الكـتالوج",
|
"catalogue": "الفهرس",
|
||||||
"downloads": "التنزيلات",
|
"downloads": "التنزيلات",
|
||||||
"search_results": "نتائج البحث",
|
"search_results": "نتائج البحث",
|
||||||
"settings": "الإعدادات",
|
"settings": "الإعدادات",
|
||||||
|
@ -40,16 +41,16 @@
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية لـ {{title}}...",
|
"downloading_metadata": "جاري تنزيل بيانات {{title}} الوصفية...",
|
||||||
"downloading": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
"downloading": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||||
"calculating_eta": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - جاري حساب الوقت المتبقي...",
|
"calculating_eta": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - جاري حساب الوقت المتبقي...",
|
||||||
"checking_files": "جارٍ فحص ملفات {{title}}... ({{percentage}} اكتمال)"
|
"checking_files": "جاري فحص ملفات {{title}}... ({{percentage}} مكتمل)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"search": "تصفية...",
|
"search": "تصفية...",
|
||||||
"developers": "المطورون",
|
"developers": "المطورون",
|
||||||
"genres": "الأنواع",
|
"genres": "الأنواع",
|
||||||
"tags": "العلامات",
|
"tags": "الوسوم",
|
||||||
"publishers": "الناشرون",
|
"publishers": "الناشرون",
|
||||||
"download_sources": "مصادر التنزيل",
|
"download_sources": "مصادر التنزيل",
|
||||||
"result_count": "{{resultCount}} نتيجة",
|
"result_count": "{{resultCount}} نتيجة",
|
||||||
|
@ -68,34 +69,34 @@
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"remove": "إزالة",
|
"remove": "إزالة",
|
||||||
"space_left_on_disk": "{{space}} متبقي على القرص",
|
"space_left_on_disk": "{{space}} متبقي على القرص",
|
||||||
"eta": "الانتهاء {{eta}}",
|
"eta": "الانتهاء المتوقع {{eta}}",
|
||||||
"calculating_eta": "جارٍ حساب الوقت المتبقي...",
|
"calculating_eta": "جاري حساب الوقت المتبقي...",
|
||||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||||
"filter": "تصفية الحزم المعاد تعبئتها",
|
"filter": "تصفية الإصدارات المعادة",
|
||||||
"requirements": "متطلبات النظام",
|
"requirements": "متطلبات النظام",
|
||||||
"minimum": "الحد الأدنى",
|
"minimum": "الحد الأدنى",
|
||||||
"recommended": "مُوصى به",
|
"recommended": "مستحسن",
|
||||||
"paused": "معلّق",
|
"paused": "معلق",
|
||||||
"release_date": "تاريخ الإصدار {{date}}",
|
"release_date": "تاريخ الإصدار {{date}}",
|
||||||
"publisher": "نشر بواسطة {{publisher}}",
|
"publisher": "نشر بواسطة {{publisher}}",
|
||||||
"hours": "ساعات",
|
"hours": "ساعات",
|
||||||
"minutes": "دقائق",
|
"minutes": "دقائق",
|
||||||
"amount_hours": "{{amount}} ساعات",
|
"amount_hours": "{{amount}} ساعة",
|
||||||
"amount_minutes": "{{amount}} دقائق",
|
"amount_minutes": "{{amount}} دقيقة",
|
||||||
"accuracy": "دقة {{accuracy}}%",
|
"accuracy": "دقة {{accuracy}}%",
|
||||||
"add_to_library": "إضافة إلى المكتبة",
|
"add_to_library": "إضافة إلى المكتبة",
|
||||||
"remove_from_library": "إزالة من المكتبة",
|
"remove_from_library": "إزالة من المكتبة",
|
||||||
"no_downloads": "لا توجد تنزيلات متاحة",
|
"no_downloads": "لا توجد تنزيلات متاحة",
|
||||||
"play_time": "لعب لمدة {{amount}}",
|
"play_time": "وقت اللعب {{amount}}",
|
||||||
"last_time_played": "آخر تشغيل {{period}}",
|
"last_time_played": "آخر مرة لعب {{period}}",
|
||||||
"not_played_yet": "لم تلعب {{title}} بعد",
|
"not_played_yet": "لم تلعب {{title}} بعد",
|
||||||
"next_suggestion": "الاقتراح التالي",
|
"next_suggestion": "الاقتراح التالي",
|
||||||
"play": "تشغيل",
|
"play": "تشغيل",
|
||||||
"deleting": "جارٍ حذف المثبت...",
|
"deleting": "جاري حذف المثبت...",
|
||||||
"close": "إغلاق",
|
"close": "إغلاق",
|
||||||
"playing_now": "يتم التشغيل الآن",
|
"playing_now": "جاري التشغيل الآن",
|
||||||
"change": "تغيير",
|
"change": "تغيير",
|
||||||
"repacks_modal_description": "اختر الحزمة المعاد تعبئتها التي تريد تنزيلها",
|
"repacks_modal_description": "اختر الإصدار المعاد الذي تريد تنزيله",
|
||||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
||||||
"download_now": "تنزيل الآن",
|
"download_now": "تنزيل الآن",
|
||||||
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
||||||
|
@ -110,12 +111,12 @@
|
||||||
"select_executable": "تحديد",
|
"select_executable": "تحديد",
|
||||||
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
||||||
"open_folder": "فتح المجلد",
|
"open_folder": "فتح المجلد",
|
||||||
"open_download_location": "عرض الملفات المحملة",
|
"open_download_location": "عرض الملفات المنزلة",
|
||||||
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
||||||
"clear": "مسح",
|
"clear": "مسح",
|
||||||
"remove_files": "إزالة الملفات",
|
"remove_files": "إزالة الملفات",
|
||||||
"remove_from_library_title": "هل أنت متأكد؟",
|
"remove_from_library_title": "هل أنت متأكد؟",
|
||||||
"remove_from_library_description": "سيؤدي هذا إلى إزالة {{game}} من مكتبتك",
|
"remove_from_library_description": "سيتم إزالة {{game}} من مكتبتك",
|
||||||
"options": "خيارات",
|
"options": "خيارات",
|
||||||
"executable_section_title": "ملف التشغيل",
|
"executable_section_title": "ملف التشغيل",
|
||||||
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||||
|
@ -123,35 +124,35 @@
|
||||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||||
"danger_zone_section_title": "منطقة الخطر",
|
"danger_zone_section_title": "منطقة الخطر",
|
||||||
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||||
"download_in_progress": "تنزيل قيد التقدم",
|
"download_in_progress": "جاري التنزيل",
|
||||||
"download_paused": "التنزيل معلق",
|
"download_paused": "التنزيل معلق",
|
||||||
"last_downloaded_option": "خيار التنزيل الأخير",
|
"last_downloaded_option": "خيار التنزيل الأخير",
|
||||||
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||||
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
||||||
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
||||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يناسب جميع الأعمار. هل تريد المتابعة؟",
|
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يكون مناسبًا لجميع الأعمار. هل تريد المتابعة؟",
|
||||||
"allow_nsfw_content": "متابعة",
|
"allow_nsfw_content": "متابعة",
|
||||||
"refuse_nsfw_content": "رجوع",
|
"refuse_nsfw_content": "رجوع",
|
||||||
"stats": "الإحصائيات",
|
"stats": "الإحصائيات",
|
||||||
"download_count": "مرات التنزيل",
|
"download_count": "التنزيلات",
|
||||||
"player_count": "اللاعبون النشطون",
|
"player_count": "اللاعبون النشطون",
|
||||||
"download_error": "خيار التنزيل هذا غير متاح",
|
"download_error": "خيار التنزيل هذا غير متاح",
|
||||||
"download": "تنزيل",
|
"download": "تنزيل",
|
||||||
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
||||||
"warning": "تحذير:",
|
"warning": "تحذير:",
|
||||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يبقى Hydra مفتوحًا حتى اكتماله. إذا أغلق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يظل Hydra مفتوحًا حتى اكتماله. إذا تم إغلاق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||||
"achievements": "الإنجازات",
|
"achievements": "الإنجازات",
|
||||||
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "حفظ سحابي",
|
"cloud_save": "حفظ سحابي",
|
||||||
"cloud_save_description": "احفظ تقدمك على السحابة واستمر في اللعب من أي جهاز",
|
"cloud_save_description": "احفظ تقدمك في السحابة واستمر في اللعب من أي جهاز",
|
||||||
"backups": "النسخ الاحتياطية",
|
"backups": "النسخ الاحتياطية",
|
||||||
"install_backup": "تثبيت",
|
"install_backup": "تثبيت",
|
||||||
"delete_backup": "حذف",
|
"delete_backup": "حذف",
|
||||||
"create_backup": "نسخة احتياطية جديدة",
|
"create_backup": "نسخة احتياطية جديدة",
|
||||||
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
||||||
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
||||||
"restoring_backup": "جارٍ استعادة النسخة الاحتياطية ({{progress}} اكتمال)...",
|
"restoring_backup": "جاري استعادة النسخة الاحتياطية ({{progress}} مكتمل)...",
|
||||||
"uploading_backup": "جارٍ رفع النسخة الاحتياطية...",
|
"uploading_backup": "جاري رفع النسخة الاحتياطية...",
|
||||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
||||||
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
||||||
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||||
|
@ -164,61 +165,67 @@
|
||||||
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||||
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||||
"manage_files": "إدارة الملفات",
|
"manage_files": "إدارة الملفات",
|
||||||
"loading_save_preview": "جارٍ البحث عن حفظات الألعاب...",
|
"loading_save_preview": "جاري البحث عن حفظات اللعبة...",
|
||||||
"wine_prefix": "بادئة Wine",
|
"wine_prefix": "بادئة Wine",
|
||||||
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||||
"launch_options": "خيارات التشغيل",
|
"launch_options": "خيارات التشغيل",
|
||||||
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
||||||
"launch_options_placeholder": "لم يتم تحديد أي معاملات",
|
"launch_options_placeholder": "لا توجد معلمات محددة",
|
||||||
"no_download_option_info": "لا توجد معلومات متاحة",
|
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||||
"backup_deletion_failed": "فشل حذف النسخة الاحتياطية",
|
"backup_deletion_failed": "فشل في حذف النسخة الاحتياطية",
|
||||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى من النسخ الاحتياطية لهذه اللعبة",
|
||||||
"achievements_not_sync": "تعرف على كيفية مزامنة إنجازاتك",
|
"achievements_not_sync": "شاهد كيفية مزامنة إنجازاتك",
|
||||||
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||||
"select_folder": "حدد المجلد",
|
"select_folder": "حدد المجلد",
|
||||||
"backup_from": "نسخة احتياطية من {{date}}",
|
"backup_from": "نسخة احتياطية من {{date}}",
|
||||||
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
||||||
"no_directory_selected": "لم يتم تحديد مجلد",
|
"no_directory_selected": "لم يتم تحديد مجلد",
|
||||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا لمعرفة المزيد.",
|
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا للمزيد من المعلومات.",
|
||||||
"reset_achievements": "إعادة تعيين الإنجازات",
|
"reset_achievements": "إعادة تعيين الإنجازات",
|
||||||
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
||||||
"reset_achievements_title": "هل أنت متأكد؟",
|
"reset_achievements_title": "هل أنت متأكد؟",
|
||||||
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
||||||
"reset_achievements_error": "فشل إعادة تعيين الإنجازات"
|
"reset_achievements_error": "فشل في إعادة تعيين الإنجازات",
|
||||||
|
"download_error_gofile_quota_exceeded": "لقد تجاوزت الحصة الشهرية لـ Gofile. يرجى الانتظار حتى إعادة تعيين الحصة.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "حساب Real-Debrid الخاص بك غير مصرح له بإجراء تنزيلات جديدة. يرجى مراجعة إعدادات الحساب والمحاولة مرة أخرى.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "هذا التنزيل غير متوفر على Real-Debrid وجلب حالة التنزيل من Real-Debrid غير متاح حاليًا.",
|
||||||
|
"download_error_not_cached_in_torbox": "هذا التنزيل غير متوفر على Torbox وجلب حالة التنزيل من Torbox غير متاح حاليًا.",
|
||||||
|
"game_removed_from_favorites": "تمت إزالة اللعبة من المفضلة",
|
||||||
|
"game_added_to_favorites": "تمت إضافة اللعبة إلى المفضلة"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "تفعيل Hydra",
|
"title": "تفعيل Hydra",
|
||||||
"installation_id": "معرف التثبيت:",
|
"installation_id": "معرف التثبيت:",
|
||||||
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا يجب أن يكون لديك هذا.",
|
"message": "إذا كنت لا تعرف أين تطلب هذا، فأنت لا يجب أن يكون لديك هذا.",
|
||||||
"activate": "تفعيل",
|
"activate": "تفعيل",
|
||||||
"loading": "جارٍ التحميل..."
|
"loading": "جاري التحميل..."
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "استئناف",
|
"resume": "استئناف",
|
||||||
"pause": "إيقاف مؤقت",
|
"pause": "إيقاف مؤقت",
|
||||||
"eta": "الانتهاء {{eta}}",
|
"eta": "الانتهاء المتوقع {{eta}}",
|
||||||
"paused": "معلّق",
|
"paused": "معلق",
|
||||||
"verifying": "جارٍ التحقق...",
|
"verifying": "جاري التحقق...",
|
||||||
"completed": "مكتمل",
|
"completed": "مكتمل",
|
||||||
"removed": "غير محمل",
|
"removed": "غير منزّل",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"filter": "تصفية الألعاب المحملة",
|
"filter": "تصفية الألعاب المنزلة",
|
||||||
"remove": "إزالة",
|
"remove": "إزالة",
|
||||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||||
"deleting": "جارٍ حذف المثبت...",
|
"deleting": "جاري حذف المثبت...",
|
||||||
"delete": "إزالة المثبت",
|
"delete": "حذف المثبت",
|
||||||
"delete_modal_title": "هل أنت متأكد؟",
|
"delete_modal_title": "هل أنت متأكد؟",
|
||||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
||||||
"install": "تثبيت",
|
"install": "تثبيت",
|
||||||
"download_in_progress": "قيد التقدم",
|
"download_in_progress": "قيد التقدم",
|
||||||
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||||
"downloads_completed": "مكتمل",
|
"downloads_completed": "مكتملة",
|
||||||
"queued": "في قائمة الانتظار",
|
"queued": "في قائمة الانتظار",
|
||||||
"no_downloads_title": "فارغ جدًا",
|
"no_downloads_title": "لا شيء هنا",
|
||||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
||||||
"checking_files": "جارٍ فحص الملفات...",
|
"checking_files": "جاري فحص الملفات...",
|
||||||
"seeding": "التوزيع",
|
"seeding": "جاري التوزيع",
|
||||||
"stop_seeding": "إيقاف التوزيع",
|
"stop_seeding": "إيقاف التوزيع",
|
||||||
"resume_seeding": "استئناف التوزيع",
|
"resume_seeding": "استئناف التوزيع",
|
||||||
"options": "إدارة"
|
"options": "إدارة"
|
||||||
|
@ -228,8 +235,8 @@
|
||||||
"change": "تحديث",
|
"change": "تحديث",
|
||||||
"notifications": "الإشعارات",
|
"notifications": "الإشعارات",
|
||||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||||
"enable_repack_list_notifications": "عند إضافة حزمة معاد تعبئتها جديدة",
|
"enable_repack_list_notifications": "عند إضافة إصدار معاد جديد",
|
||||||
"real_debrid_api_token_label": "رمز واجهة برمجة تطبيقات Real-Debrid",
|
"real_debrid_api_token_label": "رمز Real-Debrid API",
|
||||||
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
||||||
"launch_with_system": "تشغيل Hydra مع بدء النظام",
|
"launch_with_system": "تشغيل Hydra مع بدء النظام",
|
||||||
"general": "عام",
|
"general": "عام",
|
||||||
|
@ -238,21 +245,21 @@
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"api_token": "رمز API",
|
"api_token": "رمز API",
|
||||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، محدودة فقط بسرعة اتصالك بالإنترنت.",
|
||||||
"debrid_invalid_token": "رمز API غير صالح",
|
"debrid_invalid_token": "رمز API غير صالح",
|
||||||
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
"real_debrid_free_account_error": "الحساب \"{{username}}\" حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||||
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||||
"save_changes": "حفظ التغييرات",
|
"save_changes": "حفظ التغييرات",
|
||||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL للمصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||||
"validate_download_source": "تحقق",
|
"validate_download_source": "تحقق",
|
||||||
"remove_download_source": "إزالة",
|
"remove_download_source": "إزالة",
|
||||||
"add_download_source": "إضافة مصدر",
|
"add_download_source": "إضافة مصدر",
|
||||||
"download_count_zero": "لا توجد خيارات تنزيل",
|
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||||
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||||
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
||||||
"download_source_url": "عنوان URL لمصدر التنزيل",
|
"download_source_url": "عنوان مصدر التنزيل",
|
||||||
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||||
"download_source_up_to_date": "محدث",
|
"download_source_up_to_date": "محدث",
|
||||||
"download_source_errored": "خطأ",
|
"download_source_errored": "خطأ",
|
||||||
|
@ -272,13 +279,13 @@
|
||||||
"profile_visibility": "رؤية الملف الشخصي",
|
"profile_visibility": "رؤية الملف الشخصي",
|
||||||
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||||
"required_field": "هذا الحقل مطلوب",
|
"required_field": "هذا الحقل مطلوب",
|
||||||
"source_already_exists": "تمت إضافة هذا المصدر مسبقًا",
|
"source_already_exists": "هذا المصدر مضاف مسبقًا",
|
||||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالح",
|
||||||
"blocked_users": "المستخدمون المحظورون",
|
"blocked_users": "المستخدمون المحظورون",
|
||||||
"user_unblocked": "تم إلغاء حظر المستخدم",
|
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||||
"enable_achievement_notifications": "عند فتح إنجاز",
|
"enable_achievement_notifications": "عند فتح إنجاز",
|
||||||
"launch_minimized": "تشغيل Hydra مصغرًا",
|
"launch_minimized": "تشغيل Hydra مصغرًا",
|
||||||
"disable_nsfw_alert": "تعطيل تنبيه المحتوى غير اللائق",
|
"disable_nsfw_alert": "تعطيل تنبيهات المحتوى غير اللائق",
|
||||||
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
||||||
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
||||||
"account": "الحساب",
|
"account": "الحساب",
|
||||||
|
@ -296,18 +303,47 @@
|
||||||
"become_subscriber": "كن مشتركًا في Hydra Cloud",
|
"become_subscriber": "كن مشتركًا في Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "تم تعطيل التجديد التلقائي",
|
"subscription_renew_cancelled": "تم تعطيل التجديد التلقائي",
|
||||||
"subscription_renews_on": "سيتم تجديد اشتراكك في {{date}}",
|
"subscription_renews_on": "سيتم تجديد اشتراكك في {{date}}",
|
||||||
"bill_sent_until": "سيتم إرسال فاتورتك التالية حتى هذا اليوم"
|
"bill_sent_until": "سيتم إرسال فاتورتك القادمة حتى هذا اليوم",
|
||||||
|
"no_themes": "يبدو أنه ليس لديك أي سمات بعد، لكن لا تقلق، انقر هنا لإنشاء أول تحفة فنية لك.",
|
||||||
|
"editor_tab_code": "الكود",
|
||||||
|
"editor_tab_info": "معلومات",
|
||||||
|
"editor_tab_save": "حفظ",
|
||||||
|
"web_store": "المتجر الإلكتروني",
|
||||||
|
"clear_themes": "مسح",
|
||||||
|
"create_theme": "إنشاء",
|
||||||
|
"create_theme_modal_title": "إنشاء سمة مخصصة",
|
||||||
|
"create_theme_modal_description": "إنشاء سمة جديدة لتخصيص مظهر Hydra",
|
||||||
|
"theme_name": "الاسم",
|
||||||
|
"insert_theme_name": "أدخل اسم السمة",
|
||||||
|
"set_theme": "تعيين السمة",
|
||||||
|
"unset_theme": "إلغاء تعيين السمة",
|
||||||
|
"delete_theme": "حذف السمة",
|
||||||
|
"edit_theme": "تعديل السمة",
|
||||||
|
"delete_all_themes": "حذف جميع السمات",
|
||||||
|
"delete_all_themes_description": "سيؤدي هذا إلى حذف جميع السمات المخصصة الخاصة بك",
|
||||||
|
"delete_theme_description": "سيؤدي هذا إلى حذف السمة {{theme}}",
|
||||||
|
"cancel": "إلغاء",
|
||||||
|
"appearance": "المظهر",
|
||||||
|
"enable_torbox": "تفعيل Torbox",
|
||||||
|
"torbox_description": "TorBox هي خدمة seedbox متميزة تنافس أفضل الخوادم في السوق.",
|
||||||
|
"torbox_account_linked": "تم ربط حساب TorBox",
|
||||||
|
"real_debrid_account_linked": "تم ربط حساب Real-Debrid",
|
||||||
|
"name_min_length": "يجب أن يكون اسم السمة على الأقل 3 أحرف",
|
||||||
|
"import_theme": "استيراد سمة",
|
||||||
|
"import_theme_description": "ستقوم باستيراد {{theme}} من متجر السمات",
|
||||||
|
"error_importing_theme": "خطأ في استيراد السمة",
|
||||||
|
"theme_imported": "تم استيراد السمة بنجاح"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "اكتمل التنزيل",
|
"download_complete": "اكتمل التنزيل",
|
||||||
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||||
"repack_list_updated": "تم تحديث قائمة الحزم المعاد تعبئتها",
|
"repack_list_updated": "تم تحديث قائمة الإصدارات المعادة",
|
||||||
"repack_count_one": "تمت إضافة {{count}} حزمة معاد تعبئتها",
|
"repack_count_one": "تمت إضافة {{count}} إصدار معاد",
|
||||||
"repack_count_other": "تمت إضافة {{count}} حزم معاد تعبئتها",
|
"repack_count_other": "تمت إضافة {{count}} إصدارات معادة",
|
||||||
"new_update_available": "الإصدار {{version}} متوفر",
|
"new_update_available": "الإصدار {{version}} متوفر",
|
||||||
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
||||||
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
||||||
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} آخرين تم فتحهم"
|
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} أخرى تم فتحها"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "فتح Hydra",
|
"open": "فتح Hydra",
|
||||||
|
@ -319,7 +355,7 @@
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "البرامج غير مثبتة",
|
"title": "البرامج غير مثبتة",
|
||||||
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
||||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة لينكس الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "زر الإغلاق"
|
"close": "زر الإغلاق"
|
||||||
|
@ -328,16 +364,16 @@
|
||||||
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} ساعات",
|
"amount_hours": "{{amount}} ساعة",
|
||||||
"amount_minutes": "{{amount}} دقائق",
|
"amount_minutes": "{{amount}} دقيقة",
|
||||||
"last_time_played": "آخر تشغيل {{period}}",
|
"last_time_played": "آخر مرة لعب {{period}}",
|
||||||
"activity": "النشاط الأخير",
|
"activity": "النشاط الأخير",
|
||||||
"library": "المكتبة",
|
"library": "المكتبة",
|
||||||
"total_play_time": "إجمالي وقت اللعب",
|
"total_play_time": "إجمالي وقت اللعب",
|
||||||
"no_recent_activity_title": "همم... لا شيء هنا",
|
"no_recent_activity_title": "لا شيء هنا...",
|
||||||
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
||||||
"display_name": "اسم العرض",
|
"display_name": "اسم العرض",
|
||||||
"saving": "جارٍ الحفظ",
|
"saving": "جاري الحفظ",
|
||||||
"save": "حفظ",
|
"save": "حفظ",
|
||||||
"edit_profile": "تعديل الملف الشخصي",
|
"edit_profile": "تعديل الملف الشخصي",
|
||||||
"saved_successfully": "تم الحفظ بنجاح",
|
"saved_successfully": "تم الحفظ بنجاح",
|
||||||
|
@ -346,13 +382,13 @@
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||||
"sign_out": "تسجيل الخروج",
|
"sign_out": "تسجيل الخروج",
|
||||||
"playing_for": "يلعب لمدة {{amount}}",
|
"playing_for": "جاري اللعب لمدة {{amount}}",
|
||||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية بعد الآن، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||||
"add_friends": "إضافة أصدقاء",
|
"add_friends": "إضافة أصدقاء",
|
||||||
"add": "إضافة",
|
"add": "إضافة",
|
||||||
"friend_code": "رمز الصديق",
|
"friend_code": "رمز الصديق",
|
||||||
"see_profile": "عرض الملف الشخصي",
|
"see_profile": "عرض الملف الشخصي",
|
||||||
"sending": "جارٍ الإرسال",
|
"sending": "جاري الإرسال",
|
||||||
"friend_request_sent": "تم إرسال طلب الصداقة",
|
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||||
"friends": "الأصدقاء",
|
"friends": "الأصدقاء",
|
||||||
"friends_list": "قائمة الأصدقاء",
|
"friends_list": "قائمة الأصدقاء",
|
||||||
|
@ -371,19 +407,19 @@
|
||||||
"blocked_users": "المستخدمون المحظورون",
|
"blocked_users": "المستخدمون المحظورون",
|
||||||
"unblock": "إلغاء الحظر",
|
"unblock": "إلغاء الحظر",
|
||||||
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
||||||
"pending": "قيد الانتظار",
|
"pending": "معلق",
|
||||||
"no_pending_invites": "ليس لديك دعوات معلقة",
|
"no_pending_invites": "ليس لديك دعوات معلقة",
|
||||||
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
||||||
"friend_code_copied": "تم نسخ رمز الصديق",
|
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||||
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
||||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
||||||
"locked_profile": "هذا الملف الشخصي خاص",
|
"locked_profile": "هذا الملف الشخصي خاص",
|
||||||
"image_process_failure": "فشل معالجة الصورة",
|
"image_process_failure": "فشل في معالجة الصورة",
|
||||||
"required_field": "هذا الحقل مطلوب",
|
"required_field": "هذا الحقل مطلوب",
|
||||||
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
||||||
"displayname_max_length": "يجب ألا يتجاوز اسم العرض 50 حرفًا",
|
"displayname_max_length": "يجب أن لا يتجاوز اسم العرض 50 حرفًا",
|
||||||
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
"report_profile": "الإبلاغ عن هذا الملف",
|
||||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف؟",
|
||||||
"report_description": "معلومات إضافية",
|
"report_description": "معلومات إضافية",
|
||||||
"report_description_placeholder": "معلومات إضافية",
|
"report_description_placeholder": "معلومات إضافية",
|
||||||
"report": "الإبلاغ",
|
"report": "الإبلاغ",
|
||||||
|
@ -393,32 +429,32 @@
|
||||||
"report_reason_spam": "بريد عشوائي",
|
"report_reason_spam": "بريد عشوائي",
|
||||||
"report_reason_other": "أخرى",
|
"report_reason_other": "أخرى",
|
||||||
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||||
"your_friend_code": "رمز صديقك:",
|
"your_friend_code": "رمز الصديق الخاص بك:",
|
||||||
"upload_banner": "تحميل بانر",
|
"upload_banner": "رفع بانر",
|
||||||
"uploading_banner": "جارٍ تحميل البانر...",
|
"uploading_banner": "جاري رفع البانر...",
|
||||||
"background_image_updated": "تم تحديث صورة الخلفية",
|
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||||
"stats": "الإحصائيات",
|
"stats": "الإحصائيات",
|
||||||
"achievements": "إنجازات",
|
"achievements": "الإنجازات",
|
||||||
"games": "الألعاب",
|
"games": "الألعاب",
|
||||||
"top_percentile": "ال{{percentile}}% الأعلى",
|
"top_percentile": "الأعلى {{percentile}}%",
|
||||||
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
||||||
"playing": "يلعب {{game}}",
|
"playing": "جاري لعب {{game}}",
|
||||||
"achievements_unlocked": "الإنجازات المفتوحة",
|
"achievements_unlocked": "الإنجازات المفتوحة",
|
||||||
"earned_points": "النقاط المكتسبة",
|
"earned_points": "النقاط المكتسبة",
|
||||||
"show_achievements_on_profile": "عرض إنجازاتك على ملفك الشخصي",
|
"show_achievements_on_profile": "عرض إنجازاتك في ملفك الشخصي",
|
||||||
"show_points_on_profile": "عرض نقاطك المكتسبة على ملفك الشخصي"
|
"show_points_on_profile": "عرض نقاطك المكتسبة في ملفك الشخصي"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "تم فتح الإنجاز",
|
"achievement_unlocked": "تم فتح الإنجاز",
|
||||||
"user_achievements": "إنجازات {{displayName}}",
|
"user_achievements": "إنجازات {{displayName}}",
|
||||||
"your_achievements": "إنجازاتك",
|
"your_achievements": "إنجازاتك",
|
||||||
"unlocked_at": "تم الفتح في: {{date}}",
|
"unlocked_at": "تم الفتح في: {{date}}",
|
||||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لعرض هذا المحتوى",
|
||||||
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
||||||
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
||||||
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
||||||
"achievement_earn_points": "اكسب {{points}} نقطة مع هذا الإنجاز",
|
"achievement_earn_points": "احصل على {{points}} نقاط مع هذا الإنجاز",
|
||||||
"earned_points": "النقاط المكتسبة:",
|
"earned_points": "النقاط المكتسبة:",
|
||||||
"available_points": "النقاط المتاحة:",
|
"available_points": "النقاط المتاحة:",
|
||||||
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
||||||
|
@ -428,10 +464,10 @@
|
||||||
"subscribe_now": "اشترك الآن",
|
"subscribe_now": "اشترك الآن",
|
||||||
"cloud_saving": "حفظ سحابي",
|
"cloud_saving": "حفظ سحابي",
|
||||||
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||||
"animated_profile_picture": "صورة ملف شخصي متحركة",
|
"animated_profile_picture": "صورة ملف متحركة",
|
||||||
"premium_support": "دعم ممتاز",
|
"premium_support": "دعم ممتاز",
|
||||||
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
||||||
"animated_profile_banner": "بانر ملف شخصي متحرك",
|
"animated_profile_banner": "بانر ملف متحرك",
|
||||||
"hydra_cloud": "Hydra Cloud",
|
"hydra_cloud": "Hydra Cloud",
|
||||||
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
||||||
"learn_more": "معرفة المزيد"
|
"learn_more": "معرفة المزيد"
|
||||||
|
|
|
@ -189,9 +189,10 @@
|
||||||
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||||
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||||
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
|
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available.",
|
||||||
|
"game_removed_from_favorites": "Game removed from favorites",
|
||||||
|
"game_added_to_favorites": "Game added to favorites"
|
||||||
},
|
},
|
||||||
|
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
"installation_id": "Installation ID:",
|
"installation_id": "Installation ID:",
|
||||||
|
@ -303,10 +304,35 @@
|
||||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||||
"bill_sent_until": "Your next bill will be sent until this day",
|
"bill_sent_until": "Your next bill will be sent until this day",
|
||||||
|
"no_themes": "Seems like you don't have any themes yet, but no worries, click here to create your first masterpiece.",
|
||||||
|
"editor_tab_code": "Code",
|
||||||
|
"editor_tab_info": "Info",
|
||||||
|
"editor_tab_save": "Save",
|
||||||
|
"web_store": "Web store",
|
||||||
|
"clear_themes": "Clear",
|
||||||
|
"create_theme": "Create",
|
||||||
|
"create_theme_modal_title": "Create custom theme",
|
||||||
|
"create_theme_modal_description": "Create a new theme to customize Hydra's appearance",
|
||||||
|
"theme_name": "Name",
|
||||||
|
"insert_theme_name": "Insert theme name",
|
||||||
|
"set_theme": "Set theme",
|
||||||
|
"unset_theme": "Unset theme",
|
||||||
|
"delete_theme": "Delete theme",
|
||||||
|
"edit_theme": "Edit theme",
|
||||||
|
"delete_all_themes": "Delete all themes",
|
||||||
|
"delete_all_themes_description": "This will delete all your custom themes",
|
||||||
|
"delete_theme_description": "This will delete the theme {{theme}}",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"appearance": "Appearance",
|
||||||
"enable_torbox": "Enable Torbox",
|
"enable_torbox": "Enable Torbox",
|
||||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||||
"torbox_account_linked": "TorBox account linked",
|
"torbox_account_linked": "TorBox account linked",
|
||||||
"real_debrid_account_linked": "Real-Debrid account linked"
|
"real_debrid_account_linked": "Real-Debrid account linked",
|
||||||
|
"name_min_length": "Theme name must be at least 3 characters long",
|
||||||
|
"import_theme": "Import theme",
|
||||||
|
"import_theme_description": "You will import {{theme}} from the theme store",
|
||||||
|
"error_importing_theme": "Error importing theme",
|
||||||
|
"theme_imported": "Theme imported successfully"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
@ -418,6 +444,9 @@
|
||||||
"show_achievements_on_profile": "Show your achievements on your profile",
|
"show_achievements_on_profile": "Show your achievements on your profile",
|
||||||
"show_points_on_profile": "Show your earned points on your profile"
|
"show_points_on_profile": "Show your earned points on your profile"
|
||||||
},
|
},
|
||||||
|
"badge": {
|
||||||
|
"badge_description_theme_creator": "Awarded to those who created a custom theme"
|
||||||
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked",
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
"user_achievements": "{{displayName}}'s Achievements",
|
"user_achievements": "{{displayName}}'s Achievements",
|
||||||
|
|
|
@ -179,9 +179,10 @@
|
||||||
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
||||||
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||||
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
|
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
|
||||||
|
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||||
|
"game_added_to_favorites": "Jogo adicionado aos favoritos"
|
||||||
},
|
},
|
||||||
|
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
"installation_id": "ID da instalação:",
|
"installation_id": "ID da instalação:",
|
||||||
|
@ -293,10 +294,33 @@
|
||||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||||
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia",
|
||||||
|
"no_themes": "Parece que você ainda não tem nenhum tema. Não se preocupe, clique aqui para criar sua primeira obra de arte.",
|
||||||
|
"editor_tab_save": "Salvar",
|
||||||
|
"web_store": "Loja de temas",
|
||||||
|
"clear_themes": "Limpar",
|
||||||
|
"create_theme": "Criar",
|
||||||
|
"create_theme_modal_title": "Criar tema customizado",
|
||||||
|
"create_theme_modal_description": "Criar novo tema para customizar a aparência do Hydra",
|
||||||
|
"theme_name": "Nome",
|
||||||
|
"insert_theme_name": "Insira o nome do tema",
|
||||||
|
"set_theme": "Habilitar tema",
|
||||||
|
"unset_theme": "Desabilitar tema",
|
||||||
|
"delete_theme": "Deletar tema",
|
||||||
|
"edit_theme": "Editar tema",
|
||||||
|
"delete_all_themes": "Deletar todos os temas",
|
||||||
|
"delete_all_themes_description": "Isso irá deletar todos os seus temas",
|
||||||
|
"delete_theme_description": "Isso irá deletar o tema {{theme}}",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"appearance": "Aparência",
|
||||||
"enable_torbox": "Habilitar Torbox",
|
"enable_torbox": "Habilitar Torbox",
|
||||||
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||||
"torbox_account_linked": "Conta do TorBox vinculada",
|
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||||
"real_debrid_account_linked": "Conta Real-Debrid associada"
|
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
||||||
|
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
||||||
|
"import_theme": "Importar tema",
|
||||||
|
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
|
||||||
|
"error_importing_theme": "Erro ao importar tema",
|
||||||
|
"theme_imported": "Tema importado com sucesso"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
@ -418,6 +442,9 @@
|
||||||
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
||||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
|
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
|
||||||
},
|
},
|
||||||
|
"badge": {
|
||||||
|
"badge_description_theme_creator": "Concedido àqueles que criaram um tema customizado"
|
||||||
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada",
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
"your_achievements": "Suas Conquistas",
|
"your_achievements": "Suas Conquistas",
|
||||||
|
|
|
@ -183,7 +183,13 @@
|
||||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||||
"reset_achievements_title": "Вы уверены?",
|
"reset_achievements_title": "Вы уверены?",
|
||||||
"reset_achievements_success": "Достижения успешно сброшены",
|
"reset_achievements_success": "Достижения успешно сброшены",
|
||||||
"reset_achievements_error": "Не удалось сбросить достижения"
|
"reset_achievements_error": "Не удалось сбросить достижения",
|
||||||
|
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, а опрос статуса загрузки с Real-Debrid пока недоступен.",
|
||||||
|
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и опросить статус загрузки с Torbox пока невозможно.",
|
||||||
|
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||||
|
"game_removed_from_favorites": "Игра удалена из избранного"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Активировать Hydra",
|
"title": "Активировать Hydra",
|
||||||
|
@ -295,7 +301,36 @@
|
||||||
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
||||||
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
||||||
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
||||||
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
|
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня",
|
||||||
|
"no_themes": "Похоже, что у вас еще нет тем, но не волнуйтесь, нажмите здесь, чтобы создать свой первый шедевр",
|
||||||
|
"editor_tab_code": "Код",
|
||||||
|
"editor_tab_info": "Информация",
|
||||||
|
"editor_tab_save": "Сохранить",
|
||||||
|
"web_store": "Веб-магазин",
|
||||||
|
"clear_themes": "Очистить",
|
||||||
|
"create_theme": "Создать",
|
||||||
|
"create_theme_modal_title": "Создать пользовательскую тему",
|
||||||
|
"create_theme_modal_description": "Создать новую тему для настройки внешнего вида Hydra",
|
||||||
|
"theme_name": "Название",
|
||||||
|
"insert_theme_name": "Вставить название темы",
|
||||||
|
"set_theme": "Установить тему",
|
||||||
|
"unset_theme": "Снять тему",
|
||||||
|
"delete_theme": "Удалить тему",
|
||||||
|
"edit_theme": "Редактировать тему",
|
||||||
|
"delete_all_themes": "Удалить все темы",
|
||||||
|
"delete_all_themes_description": "Это удалит все ваши пользовательские темы",
|
||||||
|
"delete_theme_description": "Это приведет к удалению темы {{theme}}",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"appearance": "Внешний вид",
|
||||||
|
"enable_torbox": "Включить Torbox",
|
||||||
|
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
|
||||||
|
"torbox_account_linked": "Аккаунт TorBox привязан",
|
||||||
|
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
|
||||||
|
"name_min_length": "Название темы должно содержать не менее 3 символов",
|
||||||
|
"import_theme": "Импортировать тему",
|
||||||
|
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
|
||||||
|
"error_importing_theme": "Ошибка при импорте темы",
|
||||||
|
"theme_imported": "Тема успешно импортирована"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Загрузка завершена",
|
"download_complete": "Загрузка завершена",
|
||||||
|
@ -405,6 +440,9 @@
|
||||||
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
||||||
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
|
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
|
||||||
},
|
},
|
||||||
|
"badge": {
|
||||||
|
"badge_description_theme_creator": "Награждается тот, кто создал пользовательскую тему"
|
||||||
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Достижение разблокировано",
|
"achievement_unlocked": "Достижение разблокировано",
|
||||||
"user_achievements": "Достижения {{displayName}}",
|
"user_achievements": "Достижения {{displayName}}",
|
||||||
|
|
|
@ -3,7 +3,6 @@ import jwt from "jsonwebtoken";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import type { Auth } from "@types";
|
import type { Auth } from "@types";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
|
|
||||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
@ -11,9 +10,7 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!auth) return null;
|
if (!auth) return null;
|
||||||
const payload = jwt.decode(
|
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||||
Crypto.decrypt(auth.accessToken)
|
|
||||||
) as jwt.JwtPayload;
|
|
||||||
|
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,16 @@ import "./cloud-save/upload-save-game";
|
||||||
import "./cloud-save/delete-game-artifact";
|
import "./cloud-save/delete-game-artifact";
|
||||||
import "./cloud-save/select-game-backup-path";
|
import "./cloud-save/select-game-backup-path";
|
||||||
import "./notifications/publish-new-repacks-notification";
|
import "./notifications/publish-new-repacks-notification";
|
||||||
|
import "./themes/add-custom-theme";
|
||||||
|
import "./themes/delete-custom-theme";
|
||||||
|
import "./themes/get-all-custom-themes";
|
||||||
|
import "./themes/delete-all-custom-themes";
|
||||||
|
import "./themes/update-custom-theme";
|
||||||
|
import "./themes/open-editor-window";
|
||||||
|
import "./themes/get-custom-theme-by-id";
|
||||||
|
import "./themes/get-active-custom-theme";
|
||||||
|
import "./themes/close-editor-window";
|
||||||
|
import "./themes/toggle-custom-theme";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { Game, GameShop } from "@types";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
|
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
|
@ -46,9 +46,9 @@ const addGameToLibrary = async (
|
||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(game);
|
await createGame(game).catch(() => {});
|
||||||
|
|
||||||
createGame(game).catch(() => {});
|
updateLocalUnlockedAchievements(game);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { Crypto, HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import type { Auth } from "@types";
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentToken = await HydraApi.post("/auth/payment", {
|
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||||
refreshToken: Crypto.decrypt(auth.refreshToken),
|
refreshToken: auth.refreshToken,
|
||||||
}).then((response) => response.accessToken);
|
}).then((response) => response.accessToken);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|
12
src/main/events/themes/add-custom-theme.ts
Normal file
12
src/main/events/themes/add-custom-theme.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { Theme } from "@types";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
|
||||||
|
const addCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
|
await themesSublevel.put(theme.id, theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("addCustomTheme", addCustomTheme);
|
11
src/main/events/themes/close-editor-window.ts
Normal file
11
src/main/events/themes/close-editor-window.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const closeEditorWindow = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId?: string
|
||||||
|
) => {
|
||||||
|
WindowManager.closeEditorWindow(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("closeEditorWindow", closeEditorWindow);
|
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const deleteAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
await themesSublevel.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("deleteAllCustomThemes", deleteAllCustomThemes);
|
11
src/main/events/themes/delete-custom-theme.ts
Normal file
11
src/main/events/themes/delete-custom-theme.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const deleteCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
await themesSublevel.del(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("deleteCustomTheme", deleteCustomTheme);
|
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getActiveCustomTheme = async () => {
|
||||||
|
const allThemes = await themesSublevel.values().all();
|
||||||
|
return allThemes.find((theme) => theme.isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getActiveCustomTheme", getActiveCustomTheme);
|
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
return themesSublevel.values().all();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getAllCustomThemes", getAllCustomThemes);
|
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const getCustomThemeById = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
return themesSublevel.get(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getCustomThemeById", getCustomThemeById);
|
11
src/main/events/themes/open-editor-window.ts
Normal file
11
src/main/events/themes/open-editor-window.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const openEditorWindow = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string
|
||||||
|
) => {
|
||||||
|
WindowManager.openEditorWindow(themeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("openEditorWindow", openEditorWindow);
|
22
src/main/events/themes/toggle-custom-theme.ts
Normal file
22
src/main/events/themes/toggle-custom-theme.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const toggleCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string,
|
||||||
|
isActive: boolean
|
||||||
|
) => {
|
||||||
|
const theme = await themesSublevel.get(themeId);
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
throw new Error("Theme not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await themesSublevel.put(themeId, {
|
||||||
|
...theme,
|
||||||
|
isActive,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("toggleCustomTheme", toggleCustomTheme);
|
27
src/main/events/themes/update-custom-theme.ts
Normal file
27
src/main/events/themes/update-custom-theme.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { themesSublevel } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
|
||||||
|
const updateCustomTheme = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
themeId: string,
|
||||||
|
code: string
|
||||||
|
) => {
|
||||||
|
const theme = await themesSublevel.get(themeId);
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
throw new Error("Theme not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await themesSublevel.put(themeId, {
|
||||||
|
...theme,
|
||||||
|
code,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (theme.isActive) {
|
||||||
|
WindowManager.mainWindow?.webContents.send("css-injected", code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateCustomTheme", updateCustomTheme);
|
|
@ -13,7 +13,14 @@ const cancelGameDownload = async (
|
||||||
|
|
||||||
await DownloadManager.cancelDownload(downloadKey);
|
await DownloadManager.cancelDownload(downloadKey);
|
||||||
|
|
||||||
await downloadsSublevel.del(downloadKey);
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
|
if (!download) return;
|
||||||
|
|
||||||
|
await downloadsSublevel.put(downloadKey, {
|
||||||
|
...download,
|
||||||
|
status: "removed",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||||
|
|
|
@ -15,6 +15,7 @@ const pauseGameSeed = async (
|
||||||
|
|
||||||
await downloadsSublevel.put(downloadKey, {
|
await downloadsSublevel.put(downloadKey, {
|
||||||
...download,
|
...download,
|
||||||
|
status: "complete",
|
||||||
shouldSeed: false,
|
shouldSeed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,14 @@ const resumeGameSeed = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
if (!download) return;
|
if (!download) return;
|
||||||
|
|
||||||
await downloadsSublevel.put(levelKeys.game(shop, objectId), {
|
await downloadsSublevel.put(downloadKey, {
|
||||||
...download,
|
...download,
|
||||||
|
status: "seeding",
|
||||||
shouldSeed: true,
|
shouldSeed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,10 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
const getUserPreferences = async () =>
|
const getUserPreferences = async () =>
|
||||||
db
|
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
valueEncoding: "json",
|
||||||
valueEncoding: "json",
|
});
|
||||||
})
|
|
||||||
.then((userPreferences) => {
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
|
||||||
userPreferences.realDebridApiToken = Crypto.decrypt(
|
|
||||||
userPreferences.realDebridApiToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userPreferences?.torBoxApiToken) {
|
|
||||||
userPreferences.torBoxApiToken = Crypto.decrypt(
|
|
||||||
userPreferences.torBoxApiToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return userPreferences;
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEvent("getUserPreferences", getUserPreferences);
|
registerEvent("getUserPreferences", getUserPreferences);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { registerEvent } from "../register-event";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
import { patchUserProfile } from "../profile/update-profile";
|
import { patchUserProfile } from "../profile/update-profile";
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
|
@ -24,16 +23,6 @@ const updateUserPreferences = async (
|
||||||
patchUserProfile({ language: preferences.language }).catch(() => {});
|
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.realDebridApiToken) {
|
|
||||||
preferences.realDebridApiToken = Crypto.encrypt(
|
|
||||||
preferences.realDebridApiToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.torBoxApiToken) {
|
|
||||||
preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preferences.downloadsPath) {
|
if (!preferences.downloadsPath) {
|
||||||
preferences.downloadsPath = null;
|
preferences.downloadsPath = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const getUnlockedAchievements = async (
|
||||||
|
|
||||||
return achievementsData
|
return achievementsData
|
||||||
.map((achievementData) => {
|
.map((achievementData) => {
|
||||||
const unlockedAchiementData = unlockedAchievements.find(
|
const unlockedAchievementData = unlockedAchievements.find(
|
||||||
(localAchievement) => {
|
(localAchievement) => {
|
||||||
return (
|
return (
|
||||||
localAchievement.name.toUpperCase() ==
|
localAchievement.name.toUpperCase() ==
|
||||||
|
@ -45,11 +45,11 @@ export const getUnlockedAchievements = async (
|
||||||
? achievementData.icon
|
? achievementData.icon
|
||||||
: achievementData.icongray;
|
: achievementData.icongray;
|
||||||
|
|
||||||
if (unlockedAchiementData) {
|
if (unlockedAchievementData) {
|
||||||
return {
|
return {
|
||||||
...achievementData,
|
...achievementData,
|
||||||
unlocked: true,
|
unlocked: true,
|
||||||
unlockTime: unlockedAchiementData.unlockTime,
|
unlockTime: unlockedAchievementData.unlockTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import url from "node:url";
|
import url from "node:url";
|
||||||
|
import kill from "kill-port";
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import { logger, WindowManager } from "@main/services";
|
import { logger, WindowManager } from "@main/services";
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
|
@ -58,7 +59,7 @@ app.whenReady().then(async () => {
|
||||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadState();
|
await kill(PythonRPC.RPC_PORT).finally(() => loadState());
|
||||||
|
|
||||||
const language = await db.get<string, string>(levelKeys.language, {
|
const language = await db.get<string, string>(levelKeys.language, {
|
||||||
valueEncoding: "utf-8",
|
valueEncoding: "utf-8",
|
||||||
|
@ -85,6 +86,29 @@ const handleDeepLinkPath = (uri?: string) => {
|
||||||
|
|
||||||
if (url.host === "install-source") {
|
if (url.host === "install-source") {
|
||||||
WindowManager.redirect(`settings${url.search}`);
|
WindowManager.redirect(`settings${url.search}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.host === "profile") {
|
||||||
|
const userId = url.searchParams.get("userId");
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
WindowManager.redirect(`profile/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.host === "install-theme") {
|
||||||
|
const themeName = url.searchParams.get("theme");
|
||||||
|
const authorId = url.searchParams.get("authorId");
|
||||||
|
const authorName = url.searchParams.get("authorName");
|
||||||
|
|
||||||
|
if (themeName && authorId && authorName) {
|
||||||
|
WindowManager.redirect(
|
||||||
|
`settings?theme=${themeName}&authorId=${authorId}&authorName=${authorName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error handling deep link", uri, error);
|
logger.error("Error handling deep link", uri, error);
|
||||||
|
|
|
@ -3,3 +3,4 @@ export * from "./games";
|
||||||
export * from "./game-shop-cache";
|
export * from "./game-shop-cache";
|
||||||
export * from "./game-achievements";
|
export * from "./game-achievements";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
|
export * from "./themes";
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const levelKeys = {
|
||||||
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
||||||
user: "user",
|
user: "user",
|
||||||
auth: "auth",
|
auth: "auth",
|
||||||
|
themes: "themes",
|
||||||
gameShopCache: "gameShopCache",
|
gameShopCache: "gameShopCache",
|
||||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||||
`${shop}:${objectId}:${language}`,
|
`${shop}:${objectId}:${language}`,
|
||||||
|
|
7
src/main/level/sublevels/themes.ts
Normal file
7
src/main/level/sublevels/themes.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Theme } from "@types";
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const themesSublevel = db.sublevel<string, Theme>(levelKeys.themes, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||||
Crypto,
|
|
||||||
DownloadManager,
|
|
||||||
logger,
|
|
||||||
Ludusavi,
|
|
||||||
startMainLoop,
|
|
||||||
} from "./services";
|
|
||||||
import { RealDebridClient } from "./services/download/real-debrid";
|
import { RealDebridClient } from "./services/download/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
|
@ -38,13 +32,11 @@ export const loadState = async () => {
|
||||||
Aria2.spawn();
|
Aria2.spawn();
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
Crypto.decrypt(userPreferences.realDebridApiToken)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userPreferences?.torBoxApiToken) {
|
if (userPreferences?.torBoxApiToken) {
|
||||||
TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken));
|
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
@ -57,23 +49,21 @@ export const loadState = async () => {
|
||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
.then((games) => {
|
.then((games) => {
|
||||||
return sortBy(
|
return sortBy(games, "timestamp", "DESC");
|
||||||
games.filter((game) => game.queued),
|
|
||||||
"timestamp",
|
|
||||||
"DESC"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [nextItemOnQueue] = downloads;
|
const [nextItemOnQueue] = downloads.filter((game) => game.queued);
|
||||||
|
|
||||||
const downloadsToSeed = downloads.filter(
|
const downloadsToSeed = downloads.filter(
|
||||||
(download) =>
|
(game) =>
|
||||||
download.shouldSeed &&
|
game.shouldSeed &&
|
||||||
download.downloader === Downloader.Torrent &&
|
game.downloader === Downloader.Torrent &&
|
||||||
download.progress === 1 &&
|
game.progress === 1 &&
|
||||||
download.uri !== null
|
game.uri !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("downloadsToSeed", downloadsToSeed);
|
||||||
|
|
||||||
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
|
@ -123,9 +113,7 @@ const migrateFromSqlite = async () => {
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
...rest,
|
...rest,
|
||||||
realDebridApiToken: realDebridApiToken
|
realDebridApiToken,
|
||||||
? Crypto.encrypt(realDebridApiToken)
|
|
||||||
: null,
|
|
||||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
runAtStartup: rest.runAtStartup === 1,
|
runAtStartup: rest.runAtStartup === 1,
|
||||||
startMinimized: rest.startMinimized === 1,
|
startMinimized: rest.startMinimized === 1,
|
||||||
|
@ -191,8 +179,8 @@ const migrateFromSqlite = async () => {
|
||||||
await db.put<string, Auth>(
|
await db.put<string, Auth>(
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
accessToken: Crypto.encrypt(users[0].accessToken),
|
accessToken: users[0].accessToken,
|
||||||
refreshToken: Crypto.encrypt(users[0].refreshToken),
|
refreshToken: users[0].refreshToken,
|
||||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -23,23 +23,21 @@ const saveAchievementsOnLocal = async (
|
||||||
return gameAchievementsSublevel
|
return gameAchievementsSublevel
|
||||||
.get(levelKey)
|
.get(levelKey)
|
||||||
.then(async (gameAchievement) => {
|
.then(async (gameAchievement) => {
|
||||||
if (gameAchievement) {
|
await gameAchievementsSublevel.put(levelKey, {
|
||||||
await gameAchievementsSublevel.put(levelKey, {
|
achievements: gameAchievement?.achievements ?? [],
|
||||||
...gameAchievement,
|
unlockedAchievements: unlockedAchievements,
|
||||||
unlockedAchievements: unlockedAchievements,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!sendUpdateEvent) return;
|
if (!sendUpdateEvent) return;
|
||||||
|
|
||||||
return getUnlockedAchievements(objectId, shop, true)
|
return getUnlockedAchievements(objectId, shop, true)
|
||||||
.then((achievements) => {
|
.then((achievements) => {
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
`on-update-achievements-${objectId}-${shop}`,
|
`on-update-achievements-${objectId}-${shop}`,
|
||||||
achievements
|
achievements
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,7 +131,7 @@ export const mergeAchievements = async (
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err! instanceof SubscriptionRequiredError) {
|
if (err instanceof SubscriptionRequiredError) {
|
||||||
achievementsLogger.log(
|
achievementsLogger.log(
|
||||||
"Achievements not synchronized on API due to lack of subscription",
|
"Achievements not synchronized on API due to lack of subscription",
|
||||||
game.objectId,
|
game.objectId,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
import type { Game, UnlockedAchievement } from "@types";
|
import type { Game, UnlockedAchievement } from "@types";
|
||||||
|
|
||||||
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
export const updateLocalUnlockedAchievements = async (game: Game) => {
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
|
||||||
const achievementFileInsideDirectory =
|
const achievementFileInsideDirectory =
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -219,8 +219,10 @@ export class DownloadManager {
|
||||||
} as PauseDownloadPayload)
|
} as PauseDownloadPayload)
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
if (downloadKey === this.downloadingGameId) {
|
||||||
this.downloadingGameId = null;
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
|
this.downloadingGameId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(download: Download) {
|
static async resumeDownload(download: Download) {
|
||||||
|
@ -228,14 +230,17 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
||||||
await PythonRPC.rpc.post("/action", {
|
await PythonRPC.rpc
|
||||||
action: "cancel",
|
.post("/action", {
|
||||||
game_id: downloadKey,
|
action: "cancel",
|
||||||
});
|
game_id: downloadKey,
|
||||||
|
})
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
.catch((err) => {
|
||||||
|
logger.error("Failed to cancel game download", err);
|
||||||
|
});
|
||||||
|
|
||||||
if (downloadKey === this.downloadingGameId) {
|
if (downloadKey === this.downloadingGameId) {
|
||||||
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { isFuture, isToday } from "date-fns";
|
||||||
import { db } from "@main/level";
|
import { db } from "@main/level";
|
||||||
import { levelKeys } from "@main/level/sublevels";
|
import { levelKeys } from "@main/level/sublevels";
|
||||||
import type { Auth, User } from "@types";
|
import type { Auth, User } from "@types";
|
||||||
import { Crypto } from "./crypto";
|
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
|
@ -32,8 +31,9 @@ export class HydraApi {
|
||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||||
|
|
||||||
private static readonly secondsToMilliseconds = (seconds: number) =>
|
private static secondsToMilliseconds(seconds: number) {
|
||||||
seconds * 1000;
|
return seconds * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
private static userAuth: HydraApiUserAuth = {
|
private static userAuth: HydraApiUserAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
|
@ -81,8 +81,8 @@ export class HydraApi {
|
||||||
db.put<string, Auth>(
|
db.put<string, Auth>(
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
accessToken: Crypto.encrypt(accessToken),
|
accessToken,
|
||||||
refreshToken: Crypto.encrypt(refreshToken),
|
refreshToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
|
@ -204,12 +204,8 @@ export class HydraApi {
|
||||||
const user = result.at(1) as User | undefined;
|
const user = result.at(1) as User | undefined;
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: userAuth?.accessToken
|
authToken: userAuth?.accessToken ?? "",
|
||||||
? Crypto.decrypt(userAuth.accessToken)
|
refreshToken: userAuth?.refreshToken ?? "",
|
||||||
: "",
|
|
||||||
refreshToken: userAuth?.refreshToken
|
|
||||||
? Crypto.decrypt(userAuth.refreshToken)
|
|
||||||
: "",
|
|
||||||
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
||||||
subscription: user?.subscription
|
subscription: user?.subscription
|
||||||
? { expiresAt: user.subscription?.expiresAt }
|
? { expiresAt: user.subscription?.expiresAt }
|
||||||
|
@ -258,7 +254,7 @@ export class HydraApi {
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
...auth,
|
...auth,
|
||||||
accessToken: Crypto.encrypt(accessToken),
|
accessToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./crypto";
|
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./steam";
|
export * from "./steam";
|
||||||
export * from "./steam-250";
|
export * from "./steam-250";
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const mergeWithRemoteGames = async () => {
|
||||||
? game.playTimeInMilliseconds
|
? game.playTimeInMilliseconds
|
||||||
: localGame.playTimeInMilliseconds;
|
: localGame.playTimeInMilliseconds;
|
||||||
|
|
||||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||||
...localGame,
|
...localGame,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
lastTimePlayed: updatedLastTimePlayed,
|
lastTimePlayed: updatedLastTimePlayed,
|
||||||
|
@ -39,7 +39,7 @@ export const mergeWithRemoteGames = async () => {
|
||||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: steamGame?.name,
|
title: steamGame?.name,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
|
|
|
@ -24,6 +24,8 @@ import { isStaging } from "@main/constants";
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||||
|
|
||||||
|
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||||
|
|
||||||
private static loadMainWindowURL(hash = "") {
|
private static loadMainWindowURL(hash = "") {
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
@ -55,7 +57,7 @@ export class WindowManager {
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
titleBarOverlay: {
|
titleBarOverlay: {
|
||||||
symbolColor: "#DADBE1",
|
symbolColor: "#DADBE1",
|
||||||
color: "#151515",
|
color: "#00000000",
|
||||||
height: 34,
|
height: 34,
|
||||||
},
|
},
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
@ -201,6 +203,87 @@ export class WindowManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static openEditorWindow(themeId: string) {
|
||||||
|
if (this.mainWindow) {
|
||||||
|
const existingWindow = this.editorWindows.get(themeId);
|
||||||
|
if (existingWindow) {
|
||||||
|
if (existingWindow.isMinimized()) {
|
||||||
|
existingWindow.restore();
|
||||||
|
}
|
||||||
|
existingWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorWindow = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 720,
|
||||||
|
minWidth: 600,
|
||||||
|
minHeight: 540,
|
||||||
|
backgroundColor: "#1c1c1c",
|
||||||
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
|
...(process.platform === "linux" ? { icon } : {}),
|
||||||
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
|
titleBarOverlay: {
|
||||||
|
symbolColor: "#DADBE1",
|
||||||
|
color: "#151515",
|
||||||
|
height: 34,
|
||||||
|
},
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editorWindows.set(themeId, editorWindow);
|
||||||
|
|
||||||
|
editorWindow.removeMenu();
|
||||||
|
|
||||||
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
|
editorWindow.loadURL(
|
||||||
|
`${process.env["ELECTRON_RENDERER_URL"]}#/theme-editor?themeId=${themeId}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||||
|
hash: `theme-editor?themeId=${themeId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editorWindow.once("ready-to-show", () => {
|
||||||
|
editorWindow.show();
|
||||||
|
this.mainWindow?.webContents.openDevTools();
|
||||||
|
if (isStaging) {
|
||||||
|
editorWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editorWindow.webContents.on("before-input-event", (event, input) => {
|
||||||
|
if (input.key === "F12") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.mainWindow?.webContents.toggleDevTools();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editorWindow.on("close", () => {
|
||||||
|
this.mainWindow?.webContents.closeDevTools();
|
||||||
|
this.editorWindows.delete(themeId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static closeEditorWindow(themeId?: string) {
|
||||||
|
if (themeId) {
|
||||||
|
const editorWindow = this.editorWindows.get(themeId);
|
||||||
|
if (editorWindow) {
|
||||||
|
editorWindow.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.editorWindows.forEach((editorWindow) => {
|
||||||
|
editorWindow.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static redirect(hash: string) {
|
public static redirect(hash: string) {
|
||||||
if (!this.mainWindow) this.createMainWindow();
|
if (!this.mainWindow) this.createMainWindow();
|
||||||
this.loadMainWindowURL(hash);
|
this.loadMainWindowURL(hash);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
CatalogueSearchPayload,
|
CatalogueSearchPayload,
|
||||||
SeedingStatus,
|
SeedingStatus,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
Theme,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
|
@ -347,4 +348,30 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||||
|
|
||||||
|
/* Themes */
|
||||||
|
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),
|
||||||
|
getAllCustomThemes: () => ipcRenderer.invoke("getAllCustomThemes"),
|
||||||
|
deleteAllCustomThemes: () => ipcRenderer.invoke("deleteAllCustomThemes"),
|
||||||
|
deleteCustomTheme: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("deleteCustomTheme", themeId),
|
||||||
|
updateCustomTheme: (themeId: string, code: string) =>
|
||||||
|
ipcRenderer.invoke("updateCustomTheme", themeId, code),
|
||||||
|
getCustomThemeById: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("getCustomThemeById", themeId),
|
||||||
|
getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"),
|
||||||
|
toggleCustomTheme: (themeId: string, isActive: boolean) =>
|
||||||
|
ipcRenderer.invoke("toggleCustomTheme", themeId, isActive),
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
openEditorWindow: (themeId: string) =>
|
||||||
|
ipcRenderer.invoke("openEditorWindow", themeId),
|
||||||
|
onCssInjected: (cb: (cssString: string) => void) => {
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent, cssString: string) =>
|
||||||
|
cb(cssString);
|
||||||
|
ipcRenderer.on("css-injected", listener);
|
||||||
|
return () => ipcRenderer.removeListener("css-injected", listener);
|
||||||
|
},
|
||||||
|
closeEditorWindow: (themeId?: string) =>
|
||||||
|
ipcRenderer.invoke("closeEditorWindow", themeId),
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { downloadSourcesTable } from "./dexie";
|
||||||
import { useSubscription } from "./hooks/use-subscription";
|
import { useSubscription } from "./hooks/use-subscription";
|
||||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||||
|
|
||||||
|
import { injectCustomCss } from "./helpers";
|
||||||
import "./app.scss";
|
import "./app.scss";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
|
@ -233,6 +234,17 @@ export function App() {
|
||||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
}, [updateRepacks]);
|
}, [updateRepacks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadAndApplyTheme = async () => {
|
||||||
|
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||||
|
|
||||||
|
if (activeTheme?.code) {
|
||||||
|
injectCustomCss(activeTheme.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadAndApplyTheme();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const playAudio = useCallback(() => {
|
const playAudio = useCallback(() => {
|
||||||
const audio = new Audio(achievementSound);
|
const audio = new Audio(achievementSound);
|
||||||
audio.volume = 0.2;
|
audio.volume = 0.2;
|
||||||
|
@ -249,6 +261,14 @@ export function App() {
|
||||||
};
|
};
|
||||||
}, [playAudio]);
|
}, [playAudio]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
||||||
|
injectCustomCss(cssString);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
dispatch(closeToast());
|
dispatch(closeToast());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
29
src/renderer/src/assets/icons/badge-theme-creator.svg
Normal file
29
src/renderer/src/assets/icons/badge-theme-creator.svg
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<svg width="240" height="246" viewBox="0 0 240 246" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint0_linear_1378_2496)"/>
|
||||||
|
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint1_linear_1378_2496)"/>
|
||||||
|
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" stroke="url(#paint2_linear_1378_2496)"/>
|
||||||
|
<g opacity="0.9">
|
||||||
|
<g style="mix-blend-mode:overlay">
|
||||||
|
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" fill="black"/>
|
||||||
|
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" stroke="url(#paint3_linear_1378_2496)" stroke-width="2"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_1378_2496" x1="5.63736e-07" y1="12.92" x2="246" y2="233.08" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#0CF1CA"/>
|
||||||
|
<stop offset="1" stop-color="#1DCCEB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_1378_2496" x1="19.8951" y1="-3.50306e-06" x2="226.105" y2="246" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#0DDEBB"/>
|
||||||
|
<stop offset="1" stop-color="#052520"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_1378_2496" x1="-1.9947e-06" y1="18.0561" x2="246" y2="227.944" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.7"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_1378_2496" x1="61.9253" y1="71.6411" x2="164.664" y2="169.814" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0.2"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.8 KiB |
|
@ -6,7 +6,10 @@ export interface BackdropProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
export function Backdrop({
|
||||||
|
isClosing = false,
|
||||||
|
children,
|
||||||
|
}: Readonly<BackdropProps>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("backdrop", {
|
className={cn("backdrop", {
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function Button({
|
||||||
theme = "primary",
|
theme = "primary",
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: Readonly<ButtonProps>) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
&__content {
|
&__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
@ -104,13 +106,14 @@
|
||||||
&__container {
|
&__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__section {
|
&__section {
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
padding-bottom: globals.$spacing-unit;
|
padding-bottom: globals.$spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,10 @@ export function Sidebar() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const favoriteGames = useMemo(() => {
|
||||||
|
return sortedLibrary.filter((game) => game.favorite);
|
||||||
|
}, [sortedLibrary]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
|
@ -206,13 +210,12 @@ export function Sidebar() {
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="sidebar__section">
|
{favoriteGames.length > 0 && (
|
||||||
<small className="sidebar__section-title">{t("favorites")}</small>
|
<section className="sidebar__section">
|
||||||
|
<small className="sidebar__section-title">{t("favorites")}</small>
|
||||||
|
|
||||||
<ul className="sidebar__menu">
|
<ul className="sidebar__menu">
|
||||||
{sortedLibrary
|
{favoriteGames.map((game) => (
|
||||||
.filter((game) => game.favorite)
|
|
||||||
.map((game) => (
|
|
||||||
<SidebarGameItem
|
<SidebarGameItem
|
||||||
key={game.id}
|
key={game.id}
|
||||||
game={game}
|
game={game}
|
||||||
|
@ -220,8 +223,9 @@ export function Sidebar() {
|
||||||
getGameTitle={getGameTitle}
|
getGameTitle={getGameTitle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="sidebar__section">
|
<section className="sidebar__section">
|
||||||
<small className="sidebar__section-title">{t("my_library")}</small>
|
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$dark-background-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px globals.$border-color;
|
||||||
right: 16px;
|
right: calc(globals.$spacing-unit * 2);
|
||||||
bottom: 26px + globals.$spacing-unit;
|
// 28px is the height of the bottom panel
|
||||||
|
bottom: calc(28px + globals.$spacing-unit * 2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
|
|
||||||
export const VERSION_CODENAME = "Spectre";
|
export const VERSION_CODENAME = "Polychrome";
|
||||||
|
|
||||||
export const DOWNLOADER_NAME = {
|
export const DOWNLOADER_NAME = {
|
||||||
[Downloader.RealDebrid]: "Real-Debrid",
|
[Downloader.RealDebrid]: "Real-Debrid",
|
||||||
|
@ -14,3 +14,5 @@ export const DOWNLOADER_NAME = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
|
export const THEME_WEB_STORE_URL = "https://hydrathemes.shop";
|
||||||
|
|
|
@ -9,20 +9,32 @@ export interface SettingsContext {
|
||||||
updateUserPreferences: (values: Partial<UserPreferences>) => Promise<void>;
|
updateUserPreferences: (values: Partial<UserPreferences>) => Promise<void>;
|
||||||
setCurrentCategoryIndex: React.Dispatch<React.SetStateAction<number>>;
|
setCurrentCategoryIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||||
clearSourceUrl: () => void;
|
clearSourceUrl: () => void;
|
||||||
|
clearTheme: () => void;
|
||||||
sourceUrl: string | null;
|
sourceUrl: string | null;
|
||||||
currentCategoryIndex: number;
|
currentCategoryIndex: number;
|
||||||
blockedUsers: UserBlocks["blocks"];
|
blockedUsers: UserBlocks["blocks"];
|
||||||
fetchBlockedUsers: () => Promise<void>;
|
fetchBlockedUsers: () => Promise<void>;
|
||||||
|
appearance: {
|
||||||
|
theme: string | null;
|
||||||
|
authorId: string | null;
|
||||||
|
authorName: string | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsContext = createContext<SettingsContext>({
|
export const settingsContext = createContext<SettingsContext>({
|
||||||
updateUserPreferences: async () => {},
|
updateUserPreferences: async () => {},
|
||||||
setCurrentCategoryIndex: () => {},
|
setCurrentCategoryIndex: () => {},
|
||||||
clearSourceUrl: () => {},
|
clearSourceUrl: () => {},
|
||||||
|
clearTheme: () => {},
|
||||||
sourceUrl: null,
|
sourceUrl: null,
|
||||||
currentCategoryIndex: 0,
|
currentCategoryIndex: 0,
|
||||||
blockedUsers: [],
|
blockedUsers: [],
|
||||||
fetchBlockedUsers: async () => {},
|
fetchBlockedUsers: async () => {},
|
||||||
|
appearance: {
|
||||||
|
theme: null,
|
||||||
|
authorId: null,
|
||||||
|
authorName: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Provider } = settingsContext;
|
const { Provider } = settingsContext;
|
||||||
|
@ -34,15 +46,26 @@ export interface SettingsContextProviderProps {
|
||||||
|
|
||||||
export function SettingsContextProvider({
|
export function SettingsContextProvider({
|
||||||
children,
|
children,
|
||||||
}: SettingsContextProviderProps) {
|
}: Readonly<SettingsContextProviderProps>) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [sourceUrl, setSourceUrl] = useState<string | null>(null);
|
const [sourceUrl, setSourceUrl] = useState<string | null>(null);
|
||||||
|
const [appearance, setAppearance] = useState<{
|
||||||
|
theme: string | null;
|
||||||
|
authorId: string | null;
|
||||||
|
authorName: string | null;
|
||||||
|
}>({
|
||||||
|
theme: null,
|
||||||
|
authorId: null,
|
||||||
|
authorName: null,
|
||||||
|
});
|
||||||
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
||||||
|
|
||||||
const [blockedUsers, setBlockedUsers] = useState<UserBlocks["blocks"]>([]);
|
const [blockedUsers, setBlockedUsers] = useState<UserBlocks["blocks"]>([]);
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const defaultSourceUrl = searchParams.get("urls");
|
const defaultSourceUrl = searchParams.get("urls");
|
||||||
|
const defaultAppearanceTheme = searchParams.get("theme");
|
||||||
|
const defaultAppearanceAuthorId = searchParams.get("authorId");
|
||||||
|
const defaultAppearanceAuthorName = searchParams.get("authorName");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sourceUrl) setCurrentCategoryIndex(2);
|
if (sourceUrl) setCurrentCategoryIndex(2);
|
||||||
|
@ -54,6 +77,36 @@ export function SettingsContextProvider({
|
||||||
}
|
}
|
||||||
}, [defaultSourceUrl]);
|
}, [defaultSourceUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appearance.theme) setCurrentCategoryIndex(3);
|
||||||
|
}, [appearance.theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
defaultAppearanceTheme &&
|
||||||
|
defaultAppearanceAuthorId &&
|
||||||
|
defaultAppearanceAuthorName
|
||||||
|
) {
|
||||||
|
setAppearance({
|
||||||
|
theme: defaultAppearanceTheme,
|
||||||
|
authorId: defaultAppearanceAuthorId,
|
||||||
|
authorName: defaultAppearanceAuthorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
defaultAppearanceTheme,
|
||||||
|
defaultAppearanceAuthorId,
|
||||||
|
defaultAppearanceAuthorName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const clearTheme = useCallback(() => {
|
||||||
|
setAppearance({
|
||||||
|
theme: null,
|
||||||
|
authorId: null,
|
||||||
|
authorName: null,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchBlockedUsers = useCallback(async () => {
|
const fetchBlockedUsers = useCallback(async () => {
|
||||||
const blockedUsers = await window.electron.getBlockedUsers(12, 0);
|
const blockedUsers = await window.electron.getBlockedUsers(12, 0);
|
||||||
setBlockedUsers(blockedUsers.blocks);
|
setBlockedUsers(blockedUsers.blocks);
|
||||||
|
@ -79,9 +132,11 @@ export function SettingsContextProvider({
|
||||||
setCurrentCategoryIndex,
|
setCurrentCategoryIndex,
|
||||||
clearSourceUrl,
|
clearSourceUrl,
|
||||||
fetchBlockedUsers,
|
fetchBlockedUsers,
|
||||||
|
clearTheme,
|
||||||
currentCategoryIndex,
|
currentCategoryIndex,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
blockedUsers,
|
blockedUsers,
|
||||||
|
appearance,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -41,7 +41,7 @@ export interface UserProfileContextProviderProps {
|
||||||
export function UserProfileContextProvider({
|
export function UserProfileContextProvider({
|
||||||
children,
|
children,
|
||||||
userId,
|
userId,
|
||||||
}: UserProfileContextProviderProps) {
|
}: Readonly<UserProfileContextProviderProps>) {
|
||||||
const { userDetails } = useAppSelector((state) => state.userDetails);
|
const { userDetails } = useAppSelector((state) => state.userDetails);
|
||||||
|
|
||||||
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
||||||
|
|
18
src/renderer/src/declaration.d.ts
vendored
18
src/renderer/src/declaration.d.ts
vendored
|
@ -29,6 +29,7 @@ import type {
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
TorBoxUser,
|
TorBoxUser,
|
||||||
|
Theme,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
|
@ -279,6 +280,23 @@ declare global {
|
||||||
|
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||||
|
|
||||||
|
/* Themes */
|
||||||
|
addCustomTheme: (theme: Theme) => Promise<void>;
|
||||||
|
getAllCustomThemes: () => Promise<Theme[]>;
|
||||||
|
deleteAllCustomThemes: () => Promise<void>;
|
||||||
|
deleteCustomTheme: (themeId: string) => Promise<void>;
|
||||||
|
updateCustomTheme: (themeId: string, code: string) => Promise<void>;
|
||||||
|
getCustomThemeById: (themeId: string) => Promise<Theme | null>;
|
||||||
|
getActiveCustomTheme: () => Promise<Theme | null>;
|
||||||
|
toggleCustomTheme: (themeId: string, isActive: boolean) => Promise<void>;
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
openEditorWindow: (themeId: string) => Promise<void>;
|
||||||
|
onCssInjected: (
|
||||||
|
cb: (cssString: string) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
closeEditorWindow: (themeId?: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const toastSlice = createSlice({
|
||||||
state.title = action.payload.title;
|
state.title = action.payload.title;
|
||||||
state.message = action.payload.message;
|
state.message = action.payload.message;
|
||||||
state.type = action.payload.type;
|
state.type = action.payload.type;
|
||||||
state.duration = action.payload.duration ?? 5000;
|
state.duration = action.payload.duration ?? 2000;
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
},
|
},
|
||||||
closeToast: (state) => {
|
closeToast: (state) => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
|
import { THEME_WEB_STORE_URL } from "./constants";
|
||||||
|
|
||||||
export const formatDownloadProgress = (
|
export const formatDownloadProgress = (
|
||||||
progress?: number,
|
progress?: number,
|
||||||
|
@ -53,3 +54,36 @@ export const buildGameAchievementPath = (
|
||||||
|
|
||||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||||
new Color(color).darken(amount).alpha(alpha).toString();
|
new Color(color).darken(amount).alpha(alpha).toString();
|
||||||
|
|
||||||
|
export const injectCustomCss = (css: string) => {
|
||||||
|
try {
|
||||||
|
const currentCustomCss = document.getElementById("custom-css");
|
||||||
|
if (currentCustomCss) {
|
||||||
|
currentCustomCss.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (css.startsWith(THEME_WEB_STORE_URL)) {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.id = "custom-css";
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = css;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
} else {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = "custom-css";
|
||||||
|
style.textContent = `
|
||||||
|
${css}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("failed to inject custom css:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeCustomCss = () => {
|
||||||
|
const currentCustomCss = document.getElementById("custom-css");
|
||||||
|
if (currentCustomCss) {
|
||||||
|
currentCustomCss.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -39,7 +39,7 @@ export function useDownload() {
|
||||||
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
||||||
await window.electron.pauseGameDownload(shop, objectId);
|
await window.electron.pauseGameDownload(shop, objectId);
|
||||||
await updateLibrary();
|
await updateLibrary();
|
||||||
dispatch(clearDownload());
|
if (lastPacket?.gameId === `${shop}:${objectId}`) dispatch(clearDownload());
|
||||||
};
|
};
|
||||||
|
|
||||||
const resumeDownload = async (shop: GameShop, objectId: string) => {
|
const resumeDownload = async (shop: GameShop, objectId: string) => {
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
enum Feature {
|
enum Feature {
|
||||||
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
|
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
|
||||||
|
Torbox = "TORBOX",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFeature() {
|
export function useFeature() {
|
||||||
|
const [features, setFeatures] = useState<string[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electron.getFeatures().then((features) => {
|
window.electron.getFeatures().then((features) => {
|
||||||
localStorage.setItem("features", JSON.stringify(features || []));
|
localStorage.setItem("features", JSON.stringify(features || []));
|
||||||
|
setFeatures(features || []);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isFeatureEnabled = (feature: Feature) => {
|
const isFeatureEnabled = (feature: Feature) => {
|
||||||
const features = JSON.parse(localStorage.getItem("features") || "[]");
|
if (!features) {
|
||||||
|
const features = JSON.parse(localStorage.getItem("features") ?? "[]");
|
||||||
|
return features.includes(feature);
|
||||||
|
}
|
||||||
|
|
||||||
return features.includes(feature);
|
return features.includes(feature);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,23 +18,17 @@ import { store } from "./store";
|
||||||
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
|
|
||||||
import { SuspenseWrapper } from "./components";
|
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { addCookieInterceptor } from "./cookies";
|
import { addCookieInterceptor } from "./cookies";
|
||||||
|
|
||||||
const Home = React.lazy(() => import("./pages/home/home"));
|
|
||||||
const GameDetails = React.lazy(
|
|
||||||
() => import("./pages/game-details/game-details")
|
|
||||||
);
|
|
||||||
const Downloads = React.lazy(() => import("./pages/downloads/downloads"));
|
|
||||||
const Settings = React.lazy(() => import("./pages/settings/settings"));
|
|
||||||
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
|
|
||||||
const Profile = React.lazy(() => import("./pages/profile/profile"));
|
|
||||||
const Achievements = React.lazy(
|
|
||||||
() => import("./pages/achievements/achievements")
|
|
||||||
);
|
|
||||||
|
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import Catalogue from "./pages/catalogue/catalogue";
|
||||||
|
import Home from "./pages/home/home";
|
||||||
|
import Downloads from "./pages/downloads/downloads";
|
||||||
|
import GameDetails from "./pages/game-details/game-details";
|
||||||
|
import Settings from "./pages/settings/settings";
|
||||||
|
import Profile from "./pages/profile/profile";
|
||||||
|
import Achievements from "./pages/achievements/achievements";
|
||||||
|
import ThemeEditor from "./pages/theme-editor/theme-editor";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
||||||
|
@ -79,32 +73,16 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<App />}>
|
<Route element={<App />}>
|
||||||
<Route path="/" element={<SuspenseWrapper Component={Home} />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route
|
<Route path="/catalogue" element={<Catalogue />} />
|
||||||
path="/catalogue"
|
<Route path="/downloads" element={<Downloads />} />
|
||||||
element={<SuspenseWrapper Component={Catalogue} />}
|
<Route path="/game/:shop/:objectId" element={<GameDetails />} />
|
||||||
/>
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route
|
<Route path="/profile/:userId" element={<Profile />} />
|
||||||
path="/downloads"
|
<Route path="/achievements" element={<Achievements />} />
|
||||||
element={<SuspenseWrapper Component={Downloads} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/game/:shop/:objectId"
|
|
||||||
element={<SuspenseWrapper Component={GameDetails} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/settings"
|
|
||||||
element={<SuspenseWrapper Component={Settings} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/profile/:userId"
|
|
||||||
element={<SuspenseWrapper Component={Profile} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/achievements"
|
|
||||||
element={<SuspenseWrapper Component={Achievements} />}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/theme-editor" element={<ThemeEditor />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function DeleteGameModal({
|
||||||
onClose,
|
onClose,
|
||||||
visible,
|
visible,
|
||||||
deleteGame,
|
deleteGame,
|
||||||
}: DeleteGameModalProps) {
|
}: Readonly<DeleteGameModalProps>) {
|
||||||
const { t } = useTranslation("downloads");
|
const { t } = useTranslation("downloads");
|
||||||
|
|
||||||
const handleDeleteGame = () => {
|
const handleDeleteGame = () => {
|
||||||
|
|
|
@ -5,6 +5,14 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&__details-with-article {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
align-self: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
|
QuestionIcon,
|
||||||
ThreeBarsIcon,
|
ThreeBarsIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UnlinkIcon,
|
UnlinkIcon,
|
||||||
|
@ -31,6 +32,7 @@ import {
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
|
|
||||||
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
||||||
|
|
||||||
export interface DownloadGroupProps {
|
export interface DownloadGroupProps {
|
||||||
library: LibraryGame[];
|
library: LibraryGame[];
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -122,8 +124,12 @@ export function DownloadGroup({
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{download.downloader === Downloader.Torrent && (
|
{download.downloader === Downloader.Torrent && (
|
||||||
<small>
|
<small
|
||||||
|
className="download-group__details-with-article"
|
||||||
|
data-open-article="peers-and-seeds"
|
||||||
|
>
|
||||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||||
|
<QuestionIcon size={12} />
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -136,7 +142,14 @@ export function DownloadGroup({
|
||||||
return download.status === "seeding" &&
|
return download.status === "seeding" &&
|
||||||
download.downloader === Downloader.Torrent ? (
|
download.downloader === Downloader.Torrent ? (
|
||||||
<>
|
<>
|
||||||
<p>{t("seeding")}</p>
|
<p
|
||||||
|
data-open-article="seeding"
|
||||||
|
className="download-group__details-with-article"
|
||||||
|
>
|
||||||
|
{t("seeding")}
|
||||||
|
|
||||||
|
<QuestionIcon />
|
||||||
|
</p>
|
||||||
{uploadSpeed && <p>{uploadSpeed}/s</p>}
|
{uploadSpeed && <p>{uploadSpeed}/s</p>}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -174,7 +187,7 @@ export function DownloadGroup({
|
||||||
|
|
||||||
const deleting = isGameDeleting(game.id);
|
const deleting = isGameDeleting(game.id);
|
||||||
|
|
||||||
if (download?.progress === 1) {
|
if (game.download?.progress === 1) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: t("install"),
|
label: t("install"),
|
||||||
|
@ -189,8 +202,8 @@ export function DownloadGroup({
|
||||||
disabled: deleting,
|
disabled: deleting,
|
||||||
icon: <UnlinkIcon />,
|
icon: <UnlinkIcon />,
|
||||||
show:
|
show:
|
||||||
download.status === "seeding" &&
|
game.download?.status === "seeding" &&
|
||||||
download.downloader === Downloader.Torrent,
|
game.download?.downloader === Downloader.Torrent,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
pauseSeeding(game.shop, game.objectId);
|
pauseSeeding(game.shop, game.objectId);
|
||||||
},
|
},
|
||||||
|
@ -200,8 +213,8 @@ export function DownloadGroup({
|
||||||
disabled: deleting,
|
disabled: deleting,
|
||||||
icon: <LinkIcon />,
|
icon: <LinkIcon />,
|
||||||
show:
|
show:
|
||||||
download.status !== "seeding" &&
|
game.download?.status !== "seeding" &&
|
||||||
download.downloader === Downloader.Torrent,
|
game.download?.downloader === Downloader.Torrent,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
resumeSeeding(game.shop, game.objectId);
|
resumeSeeding(game.shop, game.objectId);
|
||||||
},
|
},
|
||||||
|
@ -217,7 +230,7 @@ export function DownloadGroup({
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGameDownloading || download?.status === "active") {
|
if (isGameDownloading) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: t("pause"),
|
label: t("pause"),
|
||||||
|
|
|
@ -8,7 +8,7 @@ import "./downloads.scss";
|
||||||
import { DeleteGameModal } from "./delete-game-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { DownloadGroup } from "./download-group";
|
import { DownloadGroup } from "./download-group";
|
||||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||||
import { orderBy, sortBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export default function Downloads() {
|
export default function Downloads() {
|
||||||
|
@ -58,24 +58,24 @@ export default function Downloads() {
|
||||||
complete: [],
|
complete: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = sortBy(library, (game) => game.download?.timestamp).reduce(
|
const result = orderBy(
|
||||||
(prev, next) => {
|
library,
|
||||||
/* Game has been manually added to the library or has been canceled */
|
(game) => game.download?.timestamp,
|
||||||
if (!next.download?.status || next.download?.status === "removed")
|
"desc"
|
||||||
return prev;
|
).reduce((prev, next) => {
|
||||||
|
/* Game has been manually added to the library */
|
||||||
|
if (!next.download) return prev;
|
||||||
|
|
||||||
/* Is downloading */
|
/* Is downloading */
|
||||||
if (lastPacket?.gameId === next.id)
|
if (lastPacket?.gameId === next.id)
|
||||||
return { ...prev, downloading: [...prev.downloading, next] };
|
return { ...prev, downloading: [...prev.downloading, next] };
|
||||||
|
|
||||||
/* Is either queued or paused */
|
/* Is either queued or paused */
|
||||||
if (next.download.queued || next.download?.status === "paused")
|
if (next.download.queued || next.download?.status === "paused")
|
||||||
return { ...prev, queued: [...prev.queued, next] };
|
return { ...prev, queued: [...prev.queued, next] };
|
||||||
|
|
||||||
return { ...prev, complete: [...prev.complete, next] };
|
return { ...prev, complete: [...prev.complete, next] };
|
||||||
},
|
}, initialValue);
|
||||||
initialValue
|
|
||||||
);
|
|
||||||
|
|
||||||
const queued = orderBy(result.queued, (game) => game.download?.timestamp, [
|
const queued = orderBy(result.queued, (game) => game.download?.timestamp, [
|
||||||
"desc",
|
"desc",
|
||||||
|
|
|
@ -16,13 +16,8 @@ import { useUserDetails } from "@renderer/hooks";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import "./game-details.scss";
|
import "./game-details.scss";
|
||||||
|
|
||||||
const HERO_HEIGHT = 300;
|
|
||||||
const HERO_ANIMATION_THRESHOLD = 25;
|
|
||||||
|
|
||||||
export function GameDetailsContent() {
|
export function GameDetailsContent() {
|
||||||
const heroRef = useRef<HTMLDivElement | null>(null);
|
const heroRef = useRef<HTMLDivElement | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
@ -61,7 +56,7 @@ export function GameDetailsContent() {
|
||||||
return t("no_shop_details");
|
return t("no_shop_details");
|
||||||
}, [shopDetails, t]);
|
}, [shopDetails, t]);
|
||||||
|
|
||||||
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
const [backdropOpacity, setBackdropOpacity] = useState(1);
|
||||||
|
|
||||||
const handleHeroLoad = async () => {
|
const handleHeroLoad = async () => {
|
||||||
const output = await average(steamUrlBuilder.libraryHero(objectId!), {
|
const output = await average(steamUrlBuilder.libraryHero(objectId!), {
|
||||||
|
@ -80,26 +75,6 @@ export function GameDetailsContent() {
|
||||||
setBackdropOpacity(1);
|
setBackdropOpacity(1);
|
||||||
}, [objectId]);
|
}, [objectId]);
|
||||||
|
|
||||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
|
||||||
const heroHeight = heroRef.current?.clientHeight ?? HERO_HEIGHT;
|
|
||||||
|
|
||||||
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
|
||||||
const opacity = Math.max(
|
|
||||||
0,
|
|
||||||
1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (scrollY >= heroHeight && !isHeaderStuck) {
|
|
||||||
setIsHeaderStuck(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollY <= heroHeight && isHeaderStuck) {
|
|
||||||
setIsHeaderStuck(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBackdropOpacity(opacity);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloudSaveButtonClick = () => {
|
const handleCloudSaveButtonClick = () => {
|
||||||
if (!userDetails) {
|
if (!userDetails) {
|
||||||
window.electron.openAuthWindow(AuthPage.SignIn);
|
window.electron.openAuthWindow(AuthPage.SignIn);
|
||||||
|
@ -122,31 +97,25 @@ export function GameDetailsContent() {
|
||||||
<div
|
<div
|
||||||
className={`game-details__wrapper ${hasNSFWContentBlocked ? "game-details__wrapper--blurred" : ""}`}
|
className={`game-details__wrapper ${hasNSFWContentBlocked ? "game-details__wrapper--blurred" : ""}`}
|
||||||
>
|
>
|
||||||
<img
|
<section className="game-details__container">
|
||||||
src={steamUrlBuilder.libraryHero(objectId!)}
|
|
||||||
className="game-details__hero-image"
|
|
||||||
alt={game?.title}
|
|
||||||
onLoad={handleHeroLoad}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section
|
|
||||||
ref={containerRef}
|
|
||||||
onScroll={onScroll}
|
|
||||||
className="game-details__container"
|
|
||||||
>
|
|
||||||
<div ref={heroRef} className="game-details__hero">
|
<div ref={heroRef} className="game-details__hero">
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.libraryHero(objectId!)}
|
||||||
|
className="game-details__hero-image"
|
||||||
|
alt={game?.title}
|
||||||
|
onLoad={handleHeroLoad}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="game-details__hero-backdrop"
|
className="game-details__hero-backdrop"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: gameColor,
|
backgroundColor: gameColor,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
opacity: Math.min(1, 1 - backdropOpactiy),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="game-details__hero-logo-backdrop"
|
className="game-details__hero-logo-backdrop"
|
||||||
style={{ opacity: backdropOpactiy }}
|
style={{ opacity: backdropOpacity }}
|
||||||
>
|
>
|
||||||
<div className="game-details__hero-content">
|
<div className="game-details__hero-content">
|
||||||
<img
|
<img
|
||||||
|
@ -173,7 +142,7 @@ export function GameDetailsContent() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
<HeroPanel />
|
||||||
|
|
||||||
<div className="game-details__description-container">
|
<div className="game-details__description-container">
|
||||||
<div className="game-details__description-content">
|
<div className="game-details__description-content">
|
||||||
|
|
|
@ -7,10 +7,11 @@ import {
|
||||||
PlusCircleIcon,
|
PlusCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
import "./hero-panel-actions.scss";
|
import "./hero-panel-actions.scss";
|
||||||
|
|
||||||
export function HeroPanelActions() {
|
export function HeroPanelActions() {
|
||||||
|
@ -39,6 +40,8 @@ export function HeroPanelActions() {
|
||||||
|
|
||||||
const { updateLibrary } = useLibrary();
|
const { updateLibrary } = useLibrary();
|
||||||
|
|
||||||
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const addGameToLibrary = async () => {
|
const addGameToLibrary = async () => {
|
||||||
|
@ -54,25 +57,24 @@ export function HeroPanelActions() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addGameToFavorites = async () => {
|
const toggleGameFavorite = async () => {
|
||||||
setToggleLibraryGameDisabled(true);
|
setToggleLibraryGameDisabled(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!objectId) throw new Error("objectId is required");
|
if (game?.favorite && objectId) {
|
||||||
await window.electron.addGameToFavorites(shop, objectId);
|
await window.electron
|
||||||
updateLibrary();
|
.removeGameFromFavorites(shop, objectId)
|
||||||
updateGame();
|
.then(() => {
|
||||||
} finally {
|
showSuccessToast(t("game_removed_from_favorites"));
|
||||||
setToggleLibraryGameDisabled(false);
|
});
|
||||||
}
|
} else {
|
||||||
};
|
if (!objectId) return;
|
||||||
|
|
||||||
const removeGameFromFavorites = async () => {
|
await window.electron.addGameToFavorites(shop, objectId).then(() => {
|
||||||
setToggleLibraryGameDisabled(true);
|
showSuccessToast(t("game_added_to_favorites"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
if (!objectId) throw new Error("objectId is required");
|
|
||||||
await window.electron.removeGameFromFavorites(shop, objectId);
|
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
updateGame();
|
updateGame();
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -188,7 +190,7 @@ export function HeroPanelActions() {
|
||||||
{gameActionButton()}
|
{gameActionButton()}
|
||||||
<div className="hero-panel-actions__separator" />
|
<div className="hero-panel-actions__separator" />
|
||||||
<Button
|
<Button
|
||||||
onClick={game.favorite ? removeGameFromFavorites : addGameToFavorites}
|
onClick={toggleGameFavorite}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className="hero-panel-actions__action"
|
className="hero-panel-actions__action"
|
||||||
|
@ -196,7 +198,6 @@ export function HeroPanelActions() {
|
||||||
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
|
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowGameOptionsModal(true)}
|
onClick={() => setShowGameOptionsModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
|
|
|
@ -9,11 +9,7 @@ import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import "./hero-panel.scss";
|
import "./hero-panel.scss";
|
||||||
|
|
||||||
export interface HeroPanelProps {
|
export function HeroPanel() {
|
||||||
isHeaderStuck: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { formatDate } = useDate();
|
const { formatDate } = useDate();
|
||||||
|
@ -54,10 +50,7 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
||||||
game?.download?.status === "paused";
|
game?.download?.status === "paused";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ backgroundColor: gameColor }} className="hero-panel">
|
||||||
style={{ backgroundColor: gameColor }}
|
|
||||||
className={`hero-panel ${isHeaderStuck ? "hero-panel--stuck" : ""}`}
|
|
||||||
>
|
|
||||||
<div className="hero-panel__content">{getInfo()}</div>
|
<div className="hero-panel__content">{getInfo()}</div>
|
||||||
<div className="hero-panel__actions">
|
<div className="hero-panel__actions">
|
||||||
<HeroPanelActions />
|
<HeroPanelActions />
|
||||||
|
|
|
@ -44,10 +44,9 @@ export function DownloadSettingsModal({
|
||||||
(state) => state.userPreferences.value
|
(state) => state.userPreferences.value
|
||||||
);
|
);
|
||||||
|
|
||||||
const getDiskFreeSpace = (path: string) => {
|
const getDiskFreeSpace = async (path: string) => {
|
||||||
window.electron.getDiskFreeSpace(path).then((result) => {
|
const result = await window.electron.getDiskFreeSpace(path);
|
||||||
setDiskFreeSpace(result.free);
|
setDiskFreeSpace(result.free);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkFolderWritePermission = useCallback(
|
const checkFolderWritePermission = useCallback(
|
||||||
|
@ -100,6 +99,7 @@ export function DownloadSettingsModal({
|
||||||
userPreferences?.downloadsPath,
|
userPreferences?.downloadsPath,
|
||||||
downloaders,
|
downloaders,
|
||||||
userPreferences?.realDebridApiToken,
|
userPreferences?.realDebridApiToken,
|
||||||
|
userPreferences?.torBoxApiToken,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleChooseDownloadsPath = async () => {
|
const handleChooseDownloadsPath = async () => {
|
||||||
|
@ -155,25 +155,30 @@ export function DownloadSettingsModal({
|
||||||
<span>{t("downloader")}</span>
|
<span>{t("downloader")}</span>
|
||||||
|
|
||||||
<div className="download-settings-modal__downloaders">
|
<div className="download-settings-modal__downloaders">
|
||||||
{downloaders.map((downloader) => (
|
{downloaders.map((downloader) => {
|
||||||
<Button
|
const shouldDisableButton =
|
||||||
key={downloader}
|
(downloader === Downloader.RealDebrid &&
|
||||||
className="download-settings-modal__downloader-option"
|
!userPreferences?.realDebridApiToken) ||
|
||||||
theme={
|
(downloader === Downloader.TorBox &&
|
||||||
selectedDownloader === downloader ? "primary" : "outline"
|
!userPreferences?.torBoxApiToken);
|
||||||
}
|
|
||||||
disabled={
|
return (
|
||||||
downloader === Downloader.RealDebrid &&
|
<Button
|
||||||
!userPreferences?.realDebridApiToken
|
key={downloader}
|
||||||
}
|
className="download-settings-modal__downloader-option"
|
||||||
onClick={() => setSelectedDownloader(downloader)}
|
theme={
|
||||||
>
|
selectedDownloader === downloader ? "primary" : "outline"
|
||||||
{selectedDownloader === downloader && (
|
}
|
||||||
<CheckCircleFillIcon className="download-settings-modal__downloader-icon" />
|
disabled={shouldDisableButton}
|
||||||
)}
|
onClick={() => setSelectedDownloader(downloader)}
|
||||||
{DOWNLOADER_NAME[downloader]}
|
>
|
||||||
</Button>
|
{selectedDownloader === downloader && (
|
||||||
))}
|
<CheckCircleFillIcon className="download-settings-modal__downloader-icon" />
|
||||||
|
)}
|
||||||
|
{DOWNLOADER_NAME[downloader]}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__display-name-container {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__display-name {
|
&__display-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -76,6 +82,12 @@
|
||||||
text-shadow: 0 0 5px rgb(0 0 0 / 40%);
|
text-shadow: 0 0 5px rgb(0 0 0 / 40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__display-name-badges-container {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__current-game {
|
&__current-game {
|
||||||
&-wrapper {
|
&-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
||||||
import "./profile-hero.scss";
|
import "./profile-hero.scss";
|
||||||
|
import { UserBadges } from "./user-badges";
|
||||||
|
|
||||||
type FriendAction =
|
type FriendAction =
|
||||||
| FriendRequestAction
|
| FriendRequestAction
|
||||||
|
@ -307,9 +308,12 @@ export function ProfileHero() {
|
||||||
|
|
||||||
<div className="profile-hero__information">
|
<div className="profile-hero__information">
|
||||||
{userProfile ? (
|
{userProfile ? (
|
||||||
<h2 className="profile-hero__display-name">
|
<div className="profile-hero__display-name-container">
|
||||||
{userProfile?.displayName}
|
<h2 className="profile-hero__display-name">
|
||||||
</h2>
|
{userProfile?.displayName}
|
||||||
|
</h2>
|
||||||
|
<UserBadges />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton width={150} height={28} />
|
<Skeleton width={150} height={28} />
|
||||||
)}
|
)}
|
||||||
|
|
40
src/renderer/src/pages/profile/profile-hero/user-badges.tsx
Normal file
40
src/renderer/src/pages/profile/profile-hero/user-badges.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import BadgeThemeCreator from "@renderer/assets/icons/badge-theme-creator.svg?react";
|
||||||
|
import "./profile-hero.scss";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { userProfileContext } from "@renderer/context";
|
||||||
|
import { UserBadge } from "@types";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export function UserBadges() {
|
||||||
|
const { t } = useTranslation("badge");
|
||||||
|
const { userProfile } = useContext(userProfileContext);
|
||||||
|
|
||||||
|
if (!userProfile?.badges?.length) return null;
|
||||||
|
|
||||||
|
const getBadgeIcon = (badge: UserBadge) => {
|
||||||
|
if (badge === "THEME_CREATOR") {
|
||||||
|
return <BadgeThemeCreator width={24} height={24} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="profile-hero__display-name-badges-container">
|
||||||
|
{userProfile.badges.map((badge) => {
|
||||||
|
const badgeIcon = getBadgeIcon(badge);
|
||||||
|
|
||||||
|
if (!badgeIcon) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`badge__${badge.toLowerCase()}`}
|
||||||
|
key={badge}
|
||||||
|
title={t(`badge_description_${badge.toLowerCase()}`)}
|
||||||
|
>
|
||||||
|
{badgeIcon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -74,7 +74,10 @@ export function ReportProfile() {
|
||||||
title={t("report_profile")}
|
title={t("report_profile")}
|
||||||
clickOutsideToClose={false}
|
clickOutsideToClose={false}
|
||||||
>
|
>
|
||||||
<form className="report-profile__form">
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="report-profile__form"
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="reason"
|
name="reason"
|
||||||
|
@ -101,12 +104,7 @@ export function ReportProfile() {
|
||||||
error={errors.description?.message}
|
error={errors.description?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button className="report-profile__submit">{t("report")}</Button>
|
||||||
className="report-profile__submit"
|
|
||||||
onClick={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
{t("report")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function AddDownloadSourceModal({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
onAddDownloadSource,
|
onAddDownloadSource,
|
||||||
}: AddDownloadSourceModalProps) {
|
}: Readonly<AddDownloadSourceModalProps>) {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
@use "../../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.settings-appearance {
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { GlobeIcon, TrashIcon, PlusIcon } from "@primer/octicons-react";
|
||||||
|
import { Button } from "@renderer/components/button/button";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AddThemeModal, DeleteAllThemesModal } from "../index";
|
||||||
|
import "./theme-actions.scss";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { THEME_WEB_STORE_URL } from "@renderer/constants";
|
||||||
|
|
||||||
|
interface ThemeActionsProps {
|
||||||
|
onListUpdated: () => void;
|
||||||
|
themesCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeActions = ({
|
||||||
|
onListUpdated,
|
||||||
|
themesCount,
|
||||||
|
}: ThemeActionsProps) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const [addThemeModalVisible, setAddThemeModalVisible] = useState(false);
|
||||||
|
const [deleteAllThemesModalVisible, setDeleteAllThemesModalVisible] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AddThemeModal
|
||||||
|
visible={addThemeModalVisible}
|
||||||
|
onClose={() => setAddThemeModalVisible(false)}
|
||||||
|
onThemeAdded={onListUpdated}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteAllThemesModal
|
||||||
|
visible={deleteAllThemesModalVisible}
|
||||||
|
onClose={() => setDeleteAllThemesModalVisible(false)}
|
||||||
|
onThemesDeleted={onListUpdated}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="settings-appearance__actions">
|
||||||
|
<div className="settings-appearance__actions-left">
|
||||||
|
<Button
|
||||||
|
theme="primary"
|
||||||
|
className="settings-appearance__button"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(THEME_WEB_STORE_URL, "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GlobeIcon />
|
||||||
|
{t("web_store")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
theme="danger"
|
||||||
|
className="settings-appearance__button"
|
||||||
|
onClick={() => setDeleteAllThemesModalVisible(true)}
|
||||||
|
disabled={themesCount < 1}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
{t("clear_themes")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-appearance__actions-right">
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
className="settings-appearance__button"
|
||||||
|
onClick={() => setAddThemeModalVisible(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
{t("create_theme")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,97 @@
|
||||||
|
@use "../../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.theme-card {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: rgba(globals.$border-color, 0.01);
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
padding: 16px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: rgba(globals.$border-color, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__colors {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__author {
|
||||||
|
font-size: 12px;
|
||||||
|
color: globals.$body-color;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(globals.$muted-color, 0.8);
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&--external {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
padding: 8px 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { PencilIcon, TrashIcon } from "@primer/octicons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button } from "@renderer/components/button/button";
|
||||||
|
import type { Theme } from "@types";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import "./theme-card.scss";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { DeleteThemeModal } from "../modals/delete-theme-modal";
|
||||||
|
import { injectCustomCss, removeCustomCss } from "@renderer/helpers";
|
||||||
|
import { THEME_WEB_STORE_URL } from "@renderer/constants";
|
||||||
|
|
||||||
|
interface ThemeCardProps {
|
||||||
|
theme: Theme;
|
||||||
|
onListUpdated: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [deleteThemeModalVisible, setDeleteThemeModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const handleSetTheme = async () => {
|
||||||
|
try {
|
||||||
|
const currentTheme = await window.electron.getCustomThemeById(theme.id);
|
||||||
|
|
||||||
|
if (!currentTheme) return;
|
||||||
|
|
||||||
|
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||||
|
|
||||||
|
if (activeTheme) {
|
||||||
|
removeCustomCss();
|
||||||
|
await window.electron.toggleCustomTheme(activeTheme.id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTheme.code) {
|
||||||
|
injectCustomCss(currentTheme.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.electron.toggleCustomTheme(currentTheme.id, true);
|
||||||
|
|
||||||
|
onListUpdated();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnsetTheme = async () => {
|
||||||
|
try {
|
||||||
|
removeCustomCss();
|
||||||
|
await window.electron.toggleCustomTheme(theme.id, false);
|
||||||
|
|
||||||
|
onListUpdated();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteThemeModal
|
||||||
|
visible={deleteThemeModalVisible}
|
||||||
|
onClose={() => setDeleteThemeModalVisible(false)}
|
||||||
|
onThemeDeleted={onListUpdated}
|
||||||
|
themeId={theme.id}
|
||||||
|
themeName={theme.name}
|
||||||
|
isActive={theme.isActive}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`theme-card ${theme.isActive ? "theme-card--active" : ""}`}
|
||||||
|
key={theme.name}
|
||||||
|
>
|
||||||
|
<div className="theme-card__header">
|
||||||
|
<div className="theme-card__header__title">{theme.name}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{theme.authorName && (
|
||||||
|
<p className="theme-card__author">
|
||||||
|
{t("by")}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="theme-card__author__name"
|
||||||
|
onClick={() => navigate(`/profile/${theme.author}`)}
|
||||||
|
>
|
||||||
|
{theme.authorName}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="theme-card__actions">
|
||||||
|
<div className="theme-card__actions__left">
|
||||||
|
{theme.isActive ? (
|
||||||
|
<Button onClick={handleUnsetTheme} theme="dark">
|
||||||
|
{t("unset_theme")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={handleSetTheme} theme="outline">
|
||||||
|
{t("set_theme")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="theme-card__actions__right">
|
||||||
|
<Button
|
||||||
|
className={
|
||||||
|
theme.code.startsWith(THEME_WEB_STORE_URL)
|
||||||
|
? "theme-card__actions__right--external"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => window.electron.openEditorWindow(theme.id)}
|
||||||
|
title={t("edit_theme")}
|
||||||
|
theme="outline"
|
||||||
|
>
|
||||||
|
<PencilIcon />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => setDeleteThemeModalVisible(true)}
|
||||||
|
title={t("delete_theme")}
|
||||||
|
theme="outline"
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
@use "../../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.theme-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
background-color: rgba(globals.$border-color, 0.01);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px dashed globals.$border-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(globals.$border-color, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: globals.$body-color;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(globals.$body-color, 0.85);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { AlertIcon } from "@primer/octicons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./theme-placeholder.scss";
|
||||||
|
import { AddThemeModal } from "../modals/add-theme-modal";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ThemePlaceholderProps {
|
||||||
|
onListUpdated: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemePlaceholder = ({ onListUpdated }: ThemePlaceholderProps) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const [addThemeModalVisible, setAddThemeModalVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AddThemeModal
|
||||||
|
visible={addThemeModalVisible}
|
||||||
|
onClose={() => setAddThemeModalVisible(false)}
|
||||||
|
onThemeAdded={onListUpdated}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="theme-placeholder"
|
||||||
|
onClick={() => setAddThemeModalVisible(true)}
|
||||||
|
>
|
||||||
|
<div className="theme-placeholder__icon">
|
||||||
|
<AlertIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="theme-placeholder__text">{t("no_themes")}</p>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
7
src/renderer/src/pages/settings/aparence/index.ts
Normal file
7
src/renderer/src/pages/settings/aparence/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export { SettingsAppearance } from "./settings-appearance";
|
||||||
|
export { AddThemeModal } from "./modals/add-theme-modal";
|
||||||
|
export { DeleteAllThemesModal } from "./modals/delete-all-themes-modal";
|
||||||
|
export { DeleteThemeModal } from "./modals/delete-theme-modal";
|
||||||
|
export { ThemeCard } from "./components/theme-card";
|
||||||
|
export { ThemePlaceholder } from "./components/theme-placeholder";
|
||||||
|
export { ThemeActions } from "./components/theme-actions";
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { Modal } from "@renderer/components/modal/modal";
|
||||||
|
import { TextField } from "@renderer/components/text-field/text-field";
|
||||||
|
import { Button } from "@renderer/components/button/button";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
|
import { Theme } from "@types";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import "./modals.scss";
|
||||||
|
|
||||||
|
interface AddThemeModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onThemeAdded: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_THEME_CODE = `
|
||||||
|
/*
|
||||||
|
Here you can edit CSS for your theme and apply it on Hydra.
|
||||||
|
There are a few classes already in place, you can use them to style the launcher.
|
||||||
|
|
||||||
|
If you want to learn more about how to run Hydra in dev mode (which will allow you to inspect the DOM and view the classes)
|
||||||
|
or how to publish your theme in the theme store, you can check the docs:
|
||||||
|
https://docs.hydralauncher.gg/
|
||||||
|
|
||||||
|
Happy hacking!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {}
|
||||||
|
|
||||||
|
/* Main content */
|
||||||
|
.container__content {}
|
||||||
|
|
||||||
|
/* Bottom panel */
|
||||||
|
.bottom-panel {}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.button {}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function AddThemeModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onThemeAdded,
|
||||||
|
}: Readonly<AddThemeModalProps>) {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
const { userDetails } = useUserDetails();
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
name: yup
|
||||||
|
.string()
|
||||||
|
.required(t("required_field"))
|
||||||
|
.min(3, t("name_min_length")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting, errors },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
async (values: FormValues) => {
|
||||||
|
const theme: Theme = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: values.name,
|
||||||
|
isActive: false,
|
||||||
|
author: userDetails?.id,
|
||||||
|
authorName: userDetails?.username,
|
||||||
|
code: DEFAULT_THEME_CODE,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await window.electron.addCustomTheme(theme);
|
||||||
|
onThemeAdded();
|
||||||
|
onClose();
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
[onClose, onThemeAdded, userDetails?.id, userDetails?.username, reset]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("create_theme_modal_title")}
|
||||||
|
description={t("create_theme_modal_description")}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="add-theme-modal__container"
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
{...register("name")}
|
||||||
|
label={t("theme_name")}
|
||||||
|
placeholder={t("insert_theme_name")}
|
||||||
|
hint={errors.name?.message}
|
||||||
|
error={errors.name?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" theme="primary" disabled={isSubmitting}>
|
||||||
|
{t("create_theme")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Button } from "@renderer/components/button/button";
|
||||||
|
import { Modal } from "@renderer/components/modal/modal";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./modals.scss";
|
||||||
|
import { removeCustomCss } from "@renderer/helpers";
|
||||||
|
|
||||||
|
interface DeleteAllThemesModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onThemesDeleted: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteAllThemesModal = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onThemesDeleted,
|
||||||
|
}: DeleteAllThemesModalProps) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const handleDeleteAllThemes = async () => {
|
||||||
|
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||||
|
|
||||||
|
if (activeTheme) {
|
||||||
|
removeCustomCss();
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.electron.deleteAllCustomThemes();
|
||||||
|
await window.electron.closeEditorWindow();
|
||||||
|
onClose();
|
||||||
|
onThemesDeleted();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("delete_all_themes")}
|
||||||
|
description={t("delete_all_themes_description")}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="delete-all-themes-modal__container">
|
||||||
|
<Button theme="outline" onClick={handleDeleteAllThemes}>
|
||||||
|
{t("delete_all_themes")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button theme="primary" onClick={onClose}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Button } from "@renderer/components/button/button";
|
||||||
|
import { Modal } from "@renderer/components/modal/modal";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./modals.scss";
|
||||||
|
import { removeCustomCss } from "@renderer/helpers";
|
||||||
|
|
||||||
|
interface DeleteThemeModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
themeId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
onThemeDeleted: () => void;
|
||||||
|
themeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteThemeModal = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
themeId,
|
||||||
|
isActive,
|
||||||
|
onThemeDeleted,
|
||||||
|
themeName,
|
||||||
|
}: DeleteThemeModalProps) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const handleDeleteTheme = async () => {
|
||||||
|
if (isActive) {
|
||||||
|
removeCustomCss();
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.electron.deleteCustomTheme(themeId);
|
||||||
|
await window.electron.closeEditorWindow(themeId);
|
||||||
|
onThemeDeleted();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("delete_theme")}
|
||||||
|
description={t("delete_theme_description", { theme: themeName })}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="delete-all-themes-modal__container">
|
||||||
|
<Button theme="outline" onClick={handleDeleteTheme}>
|
||||||
|
{t("delete_theme")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button theme="primary" onClick={onClose}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { Button } from "@renderer/components/button/button";
|
||||||
|
import { Modal } from "@renderer/components/modal/modal";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./modals.scss";
|
||||||
|
import { Theme } from "@types";
|
||||||
|
import { injectCustomCss, removeCustomCss } from "@renderer/helpers";
|
||||||
|
import { useToast } from "@renderer/hooks";
|
||||||
|
import { THEME_WEB_STORE_URL } from "@renderer/constants";
|
||||||
|
import { logger } from "@renderer/logger";
|
||||||
|
|
||||||
|
interface ImportThemeModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onThemeImported: () => void;
|
||||||
|
themeName: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportThemeModal = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onThemeImported,
|
||||||
|
themeName,
|
||||||
|
authorId,
|
||||||
|
authorName,
|
||||||
|
}: ImportThemeModalProps) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const handleImportTheme = async () => {
|
||||||
|
const theme: Theme = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: themeName,
|
||||||
|
isActive: false,
|
||||||
|
author: authorId,
|
||||||
|
authorName: authorName,
|
||||||
|
code: `${THEME_WEB_STORE_URL}/themes/${themeName.toLowerCase()}/theme.css`,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electron.addCustomTheme(theme);
|
||||||
|
|
||||||
|
const currentTheme = await window.electron.getCustomThemeById(theme.id);
|
||||||
|
|
||||||
|
if (!currentTheme) return;
|
||||||
|
|
||||||
|
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||||
|
|
||||||
|
if (activeTheme) {
|
||||||
|
removeCustomCss();
|
||||||
|
await window.electron.toggleCustomTheme(activeTheme.id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTheme.code) {
|
||||||
|
injectCustomCss(currentTheme.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.electron.toggleCustomTheme(currentTheme.id, true);
|
||||||
|
onThemeImported();
|
||||||
|
showSuccessToast(t("theme_imported"));
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
showErrorToast(t("error_importing_theme"));
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("import_theme")}
|
||||||
|
description={t("import_theme_description", { theme: themeName })}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="delete-all-themes-modal__container">
|
||||||
|
<Button theme="outline" onClick={handleImportTheme}>
|
||||||
|
{t("import_theme")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button theme="primary" onClick={onClose}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
15
src/renderer/src/pages/settings/aparence/modals/modals.scss
Normal file
15
src/renderer/src/pages/settings/aparence/modals/modals.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.add-theme-modal {
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-all-themes-modal__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.settings-appearance {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__themes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&__theme {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: rgba(globals.$border-color, 0.01);
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
padding: 16px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: rgba(globals.$border-color, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__colors {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__author {
|
||||||
|
font-size: 12px;
|
||||||
|
color: globals.$body-color;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(globals.$muted-color, 0.8);
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
Button {
|
||||||
|
padding: 8px 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-themes {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
background-color: rgba(globals.$border-color, 0.01);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px dashed globals.$border-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(globals.$border-color, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: globals.$body-color;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(globals.$body-color, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
123
src/renderer/src/pages/settings/aparence/settings-appearance.tsx
Normal file
123
src/renderer/src/pages/settings/aparence/settings-appearance.tsx
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
|
import "./settings-appearance.scss";
|
||||||
|
import { ThemeActions, ThemeCard, ThemePlaceholder } from "./index";
|
||||||
|
import type { Theme } from "@types";
|
||||||
|
import { ImportThemeModal } from "./modals/import-theme-modal";
|
||||||
|
import { settingsContext } from "@renderer/context";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface SettingsAppearanceProps {
|
||||||
|
appearance: {
|
||||||
|
theme: string | null;
|
||||||
|
authorId: string | null;
|
||||||
|
authorName: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsAppearance({
|
||||||
|
appearance,
|
||||||
|
}: Readonly<SettingsAppearanceProps>) {
|
||||||
|
const [themes, setThemes] = useState<Theme[]>([]);
|
||||||
|
const [isImportThemeModalVisible, setIsImportThemeModalVisible] =
|
||||||
|
useState(false);
|
||||||
|
const [importTheme, setImportTheme] = useState<{
|
||||||
|
theme: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [hasShownModal, setHasShownModal] = useState(false);
|
||||||
|
|
||||||
|
const { clearTheme } = useContext(settingsContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const loadThemes = useCallback(async () => {
|
||||||
|
const themesList = await window.electron.getAllCustomThemes();
|
||||||
|
setThemes(themesList);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadThemes();
|
||||||
|
}, [loadThemes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onCssInjected(() => {
|
||||||
|
loadThemes();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [loadThemes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
appearance.theme &&
|
||||||
|
appearance.authorId &&
|
||||||
|
appearance.authorName &&
|
||||||
|
!hasShownModal
|
||||||
|
) {
|
||||||
|
setIsImportThemeModalVisible(true);
|
||||||
|
setImportTheme({
|
||||||
|
theme: appearance.theme,
|
||||||
|
authorId: appearance.authorId,
|
||||||
|
authorName: appearance.authorName,
|
||||||
|
});
|
||||||
|
setHasShownModal(true);
|
||||||
|
|
||||||
|
navigate("/settings", { replace: true });
|
||||||
|
clearTheme();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
appearance.theme,
|
||||||
|
appearance.authorId,
|
||||||
|
appearance.authorName,
|
||||||
|
navigate,
|
||||||
|
hasShownModal,
|
||||||
|
clearTheme,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onThemeImported = useCallback(() => {
|
||||||
|
setIsImportThemeModalVisible(false);
|
||||||
|
setImportTheme(null);
|
||||||
|
loadThemes();
|
||||||
|
}, [loadThemes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-appearance">
|
||||||
|
<ThemeActions onListUpdated={loadThemes} themesCount={themes.length} />
|
||||||
|
|
||||||
|
<div className="settings-appearance__themes">
|
||||||
|
{!themes.length ? (
|
||||||
|
<ThemePlaceholder onListUpdated={loadThemes} />
|
||||||
|
) : (
|
||||||
|
[...themes]
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.updatedAt).getTime() -
|
||||||
|
new Date(a.updatedAt).getTime()
|
||||||
|
)
|
||||||
|
.map((theme) => (
|
||||||
|
<ThemeCard
|
||||||
|
key={theme.id}
|
||||||
|
theme={theme}
|
||||||
|
onListUpdated={loadThemes}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{importTheme && (
|
||||||
|
<ImportThemeModal
|
||||||
|
visible={isImportThemeModalVisible}
|
||||||
|
onClose={() => {
|
||||||
|
setIsImportThemeModalVisible(false);
|
||||||
|
clearTheme();
|
||||||
|
setHasShownModal(false);
|
||||||
|
}}
|
||||||
|
onThemeImported={onThemeImported}
|
||||||
|
themeName={importTheme.theme}
|
||||||
|
authorId={importTheme.authorId}
|
||||||
|
authorName={importTheme.authorName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -63,7 +63,7 @@ export function SettingsAccount() {
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [fetchUserDetails, updateUserDetails, showSuccessToast]);
|
}, [fetchUserDetails, updateUserDetails, t, showSuccessToast]);
|
||||||
|
|
||||||
const visibilityOptions = [
|
const visibilityOptions = [
|
||||||
{ value: "PUBLIC", label: t("public") },
|
{ value: "PUBLIC", label: t("public") },
|
||||||
|
|
|
@ -86,12 +86,12 @@ export function SettingsRealDebrid() {
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
label={t("enable_real_debrid")}
|
label={t("enable_real_debrid")}
|
||||||
checked={form.useRealDebrid}
|
checked={form.useRealDebrid}
|
||||||
onChange={() =>
|
onChange={() => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
useRealDebrid: !form.useRealDebrid,
|
useRealDebrid: !form.useRealDebrid,
|
||||||
}))
|
}));
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{form.useRealDebrid && (
|
{form.useRealDebrid && (
|
||||||
|
|
|
@ -10,9 +10,10 @@ import {
|
||||||
SettingsContextProvider,
|
SettingsContextProvider,
|
||||||
} from "@renderer/context";
|
} from "@renderer/context";
|
||||||
import { SettingsAccount } from "./settings-account";
|
import { SettingsAccount } from "./settings-account";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useFeature, useUserDetails } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import "./settings.scss";
|
import "./settings.scss";
|
||||||
|
import { SettingsAppearance } from "./aparence/settings-appearance";
|
||||||
import { SettingsTorbox } from "./settings-torbox";
|
import { SettingsTorbox } from "./settings-torbox";
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
|
@ -20,20 +21,36 @@ export default function Settings() {
|
||||||
|
|
||||||
const { userDetails } = useUserDetails();
|
const { userDetails } = useUserDetails();
|
||||||
|
|
||||||
|
const { isFeatureEnabled, Feature } = useFeature();
|
||||||
|
|
||||||
|
const isTorboxEnabled = isFeatureEnabled(Feature.Torbox);
|
||||||
|
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
const categories = [
|
const categories = [
|
||||||
{ tabLabel: t("general"), contentTitle: t("general") },
|
{ tabLabel: t("general"), contentTitle: t("general") },
|
||||||
{ tabLabel: t("behavior"), contentTitle: t("behavior") },
|
{ tabLabel: t("behavior"), contentTitle: t("behavior") },
|
||||||
{ tabLabel: t("download_sources"), contentTitle: t("download_sources") },
|
{ tabLabel: t("download_sources"), contentTitle: t("download_sources") },
|
||||||
{
|
{
|
||||||
tabLabel: (
|
tabLabel: t("appearance"),
|
||||||
<>
|
contentTitle: t("appearance"),
|
||||||
<img src={torBoxLogo} alt="TorBox" style={{ width: 13 }} />
|
|
||||||
Torbox
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
contentTitle: "TorBox",
|
|
||||||
},
|
},
|
||||||
|
...(isTorboxEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
tabLabel: (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={torBoxLogo}
|
||||||
|
alt="TorBox"
|
||||||
|
style={{ width: 13, height: 13 }}
|
||||||
|
/>{" "}
|
||||||
|
Torbox
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
contentTitle: "TorBox",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{ tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" },
|
{ tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -43,12 +60,12 @@ export default function Settings() {
|
||||||
{ tabLabel: t("account"), contentTitle: t("account") },
|
{ tabLabel: t("account"), contentTitle: t("account") },
|
||||||
];
|
];
|
||||||
return categories;
|
return categories;
|
||||||
}, [userDetails, t]);
|
}, [userDetails, t, isTorboxEnabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContextProvider>
|
<SettingsContextProvider>
|
||||||
<SettingsContextConsumer>
|
<SettingsContextConsumer>
|
||||||
{({ currentCategoryIndex, setCurrentCategoryIndex }) => {
|
{({ currentCategoryIndex, setCurrentCategoryIndex, appearance }) => {
|
||||||
const renderCategory = () => {
|
const renderCategory = () => {
|
||||||
if (currentCategoryIndex === 0) {
|
if (currentCategoryIndex === 0) {
|
||||||
return <SettingsGeneral />;
|
return <SettingsGeneral />;
|
||||||
|
@ -63,10 +80,14 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentCategoryIndex === 3) {
|
if (currentCategoryIndex === 3) {
|
||||||
return <SettingsTorbox />;
|
return <SettingsAppearance appearance={appearance} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentCategoryIndex === 4) {
|
if (currentCategoryIndex === 4) {
|
||||||
|
return <SettingsTorbox />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCategoryIndex === 5) {
|
||||||
return <SettingsRealDebrid />;
|
return <SettingsRealDebrid />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +100,7 @@ export default function Settings() {
|
||||||
<section className="settings__categories">
|
<section className="settings__categories">
|
||||||
{categories.map((category, index) => (
|
{categories.map((category, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={index}
|
key={category.contentTitle}
|
||||||
theme={
|
theme={
|
||||||
currentCategoryIndex === index ? "primary" : "outline"
|
currentCategoryIndex === index ? "primary" : "outline"
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,12 +106,10 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||||
<div className="user-friend-item__container">
|
<div className="user-friend-item__container">
|
||||||
<div className="user-friend-item__button">
|
<div className="user-friend-item__button">
|
||||||
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
||||||
|
|
||||||
<div className="user-friend-item__button__content">
|
<div className="user-friend-item__button__content">
|
||||||
<p className="user-friend-item__display-name">{displayName}</p>
|
<p className="user-friend-item__display-name">{displayName}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="user-friend-item__button__actions">
|
<div className="user-friend-item__button__actions">
|
||||||
{getRequestActions()}
|
{getRequestActions()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -133,7 +131,6 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||||
{getRequestDescription()}
|
{getRequestDescription()}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="user-friend-item__button__actions">
|
<div className="user-friend-item__button__actions">
|
||||||
{getRequestActions()}
|
{getRequestActions()}
|
||||||
</div>
|
</div>
|
||||||
|
|
77
src/renderer/src/pages/theme-editor/theme-editor.scss
Normal file
77
src/renderer/src/pages/theme-editor/theme-editor.scss
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.theme-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: calc(globals.$spacing-unit + 1px);
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
font-size: 8px;
|
||||||
|
z-index: 50;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&--darwin {
|
||||||
|
padding-top: calc(globals.$spacing-unit * 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
display: flex;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
background-color: globals.$muted-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
padding: globals.$spacing-unit globals.$spacing-unit * 2;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 50;
|
||||||
|
|
||||||
|
&-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: darken(globals.$dark-background-color, 2%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
src/renderer/src/pages/theme-editor/theme-editor.tsx
Normal file
98
src/renderer/src/pages/theme-editor/theme-editor.tsx
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import "./theme-editor.scss";
|
||||||
|
import Editor from "@monaco-editor/react";
|
||||||
|
import { Theme } from "@types";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { Button } from "@renderer/components";
|
||||||
|
import { CheckIcon } from "@primer/octicons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
export default function ThemeEditor() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [theme, setTheme] = useState<Theme | null>(null);
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
|
|
||||||
|
const themeId = searchParams.get("themeId");
|
||||||
|
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (themeId) {
|
||||||
|
window.electron.getCustomThemeById(themeId).then((loadedTheme) => {
|
||||||
|
if (loadedTheme) {
|
||||||
|
setTheme(loadedTheme);
|
||||||
|
setCode(loadedTheme.code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [themeId]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
if (theme) {
|
||||||
|
await window.electron.updateCustomTheme(theme.id, code);
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
}
|
||||||
|
}, [code, theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [code, handleSave, theme]);
|
||||||
|
|
||||||
|
const handleEditorChange = (value: string | undefined) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setCode(value);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="theme-editor">
|
||||||
|
<div
|
||||||
|
className={cn("theme-editor__header", {
|
||||||
|
"theme-editor__header--darwin": window.electron.platform === "darwin",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<h1>{theme?.name}</h1>
|
||||||
|
{hasUnsavedChanges && (
|
||||||
|
<div className="theme-editor__header__status"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
theme="vs-dark"
|
||||||
|
defaultLanguage="css"
|
||||||
|
value={code}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 14,
|
||||||
|
lineNumbers: "on",
|
||||||
|
wordWrap: "on",
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="theme-editor__footer">
|
||||||
|
<div className="theme-editor__footer-actions">
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
<CheckIcon />
|
||||||
|
{t("editor_tab_save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -130,6 +130,8 @@ export interface UserProfileCurrentGame extends Omit<GameRunning, "objectId"> {
|
||||||
|
|
||||||
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
|
|
||||||
|
export type UserBadge = "THEME_CREATOR";
|
||||||
|
|
||||||
export interface UserDetails {
|
export interface UserDetails {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -164,6 +166,7 @@ export interface UserProfile {
|
||||||
quirks: {
|
quirks: {
|
||||||
backupsPerGameLimit: number;
|
backupsPerGameLimit: number;
|
||||||
};
|
};
|
||||||
|
badges: UserBadge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
|
@ -296,3 +299,4 @@ export * from "./download.types";
|
||||||
export * from "./ludusavi.types";
|
export * from "./ludusavi.types";
|
||||||
export * from "./how-long-to-beat.types";
|
export * from "./how-long-to-beat.types";
|
||||||
export * from "./level.types";
|
export * from "./level.types";
|
||||||
|
export * from "./theme.types";
|
||||||
|
|
10
src/types/theme.types.ts
Normal file
10
src/types/theme.types.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export interface Theme {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
author?: string;
|
||||||
|
authorName?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
code: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
37
yarn.lock
37
yarn.lock
|
@ -1790,6 +1790,20 @@
|
||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
tmp-promise "^3.0.2"
|
tmp-promise "^3.0.2"
|
||||||
|
|
||||||
|
"@monaco-editor/loader@^1.4.0":
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
|
||||||
|
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
|
||||||
|
dependencies:
|
||||||
|
state-local "^1.0.6"
|
||||||
|
|
||||||
|
"@monaco-editor/react@^4.6.0":
|
||||||
|
version "4.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
|
||||||
|
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
|
||||||
|
dependencies:
|
||||||
|
"@monaco-editor/loader" "^1.4.0"
|
||||||
|
|
||||||
"@napi-rs/nice-android-arm-eabi@1.0.1":
|
"@napi-rs/nice-android-arm-eabi@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936"
|
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936"
|
||||||
|
@ -5851,6 +5865,11 @@ get-symbol-description@^1.1.0:
|
||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
get-intrinsic "^1.2.6"
|
get-intrinsic "^1.2.6"
|
||||||
|
|
||||||
|
get-them-args@1.3.2:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-them-args/-/get-them-args-1.3.2.tgz#74a20ba8a4abece5ae199ad03f2bcc68fdfc9ba5"
|
||||||
|
integrity sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==
|
||||||
|
|
||||||
getopts@2.3.0:
|
getopts@2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4"
|
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4"
|
||||||
|
@ -6876,6 +6895,14 @@ keyv@^4.0.0, keyv@^4.5.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer "3.0.1"
|
json-buffer "3.0.1"
|
||||||
|
|
||||||
|
kill-port@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/kill-port/-/kill-port-2.0.1.tgz#e5e18e2706b13d54320938be42cb7d40609b15cf"
|
||||||
|
integrity sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==
|
||||||
|
dependencies:
|
||||||
|
get-them-args "1.3.2"
|
||||||
|
shell-exec "1.0.2"
|
||||||
|
|
||||||
knex@^3.1.0:
|
knex@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c"
|
resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c"
|
||||||
|
@ -8599,6 +8626,11 @@ shebang-regex@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||||
|
|
||||||
|
shell-exec@1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756"
|
||||||
|
integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==
|
||||||
|
|
||||||
side-channel-list@^1.0.0:
|
side-channel-list@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
||||||
|
@ -8776,6 +8808,11 @@ stat-mode@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465"
|
resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465"
|
||||||
integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==
|
integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==
|
||||||
|
|
||||||
|
state-local@^1.0.6:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
|
||||||
|
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue