Merge branch 'main' into feature/migration-to-scss

This commit is contained in:
Hachi-R 2025-01-16 11:30:09 -03:00
commit ee4c564698
217 changed files with 8411 additions and 3084 deletions

View file

@ -1,146 +1,417 @@
{
"language_name": "اَلْعَرَبِيَّةُ",
"app": {
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
},
"home": {
"featured": "مميّز",
"surprise_me": "فاجئني",
"no_results": "لم يتم العثور على نتائج"
"featured": "مُتَمَيِّز",
"surprise_me": "فَاجِئْنِي",
"no_results": "لَمْ يُعْثَرْ عَلَى نَتائِج",
"start_typing": "اِبْدَأْ بِالْكِتَابَةِ لِلْبَحْثِ...",
"hot": "اَلْأَكْثَرُ شُيُوعًا الْآن",
"weekly": "📅 أَفْضَلُ أَلْعَابِ الْأُسْبُوعِ",
"achievements": "🏆 أَلْعَابٌ لِلتَّغَلُّبِ عَلَيْهَا"
},
"sidebar": {
"catalogue": "قائمة الألعاب",
"downloads": "التحميلات",
"settings": "إعدادات",
"my_library": "مكتبتي",
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
"paused": "{{title}} (متوقف)",
"downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)",
"filter": "بحث في المكتبة",
"home": "الرئيسية"
"catalogue": "الْفِهْرِسُ",
"downloads": "التَّنْزِيلَاتُ",
"settings": "الإعْدَادَاتُ",
"my_library": "مَكْتَبَتِي",
"downloading_metadata": "{{title}} (جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...)",
"paused": "{{title}} (مُوْقَفٌ)",
"downloading": "{{title}} ({{percentage}} - جَارٍ التَّنْزِيلُ...)",
"filter": "تَصْفِيَةُ الْمَكْتَبَةِ",
"home": "الرَّئِيسِيَّةُ",
"queued": "{{title}} (فِي الْانْتِظَارِ)",
"game_has_no_executable": "اللُّعْبَةُ لَيْسَ لَدَيْهَا مِلَفٌّ تَنْفِيذِيٌّ مُحَدَّدٌ",
"sign_in": "تَسْجِيلُ الدُّخُولِ",
"friends": "الْأَصْدِقَاءُ",
"need_help": "هَلْ تَحْتَاجُ إِلَى مُسَاعَدَةٍ؟"
},
"header": {
"search": "ابحث عن الألعاب",
"home": "الرئيسية",
"catalogue": "قائمة الألعاب",
"downloads": "التحميلات",
"search_results": "نتائج البحث",
"settings": "إعدادات"
"search": "بَحْثُ الْأَلْعَابِ",
"home": "الرَّئِيسِيَّةُ",
"catalogue": "الْفِهْرِسُ",
"downloads": "التَّنْزِيلَاتُ",
"search_results": "نَتائِجُ الْبَحْثِ",
"settings": "الإعْدَادَاتُ",
"version_available_install": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِإِعَادَةِ التَّشْغِيلِ وَالتَّثْبِيتِ.",
"version_available_download": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِلتَّنْزِيلِ."
},
"bottom_panel": {
"no_downloads_in_progress": "لا يوجد تنزيلات جارية",
"downloading_metadata": "جارٍ تنزيل بيانات وصف {{title}}",
"downloading": "جارٍ تنزيل {{title}}… ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}"
"no_downloads_in_progress": "لَا تَوْجَدُ تَنْزِيلَاتٌ جَارِيَةٌ",
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ لِـ {{title}}...",
"downloading": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - الِاكْتِمَالُ {{eta}} - {{speed}}",
"calculating_eta": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
"checking_files": "جَارٍ التَّحَقُّقُ مِنْ مَلَفَّاتِ {{title}}... ({{percentage}} مَكْتُومٌ)"
},
"catalogue": {
"next_page": "الصفحة التالية",
"previous_page": "الصفحة السابقة"
"search": "تَصْفِيَةٌ...",
"developers": "الْمُطَوِّرُونَ",
"genres": "الْأَنْوَاعُ",
"tags": "الْعَلَامَاتُ",
"publishers": "النَّاشِرُونَ",
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
"result_count": "{{resultCount}} نَتائِجُ",
"filter_count": "{{filterCount}} مَتَوَفِّرٌ",
"clear_filters": "مَسْحُ {{filterCount}} الْمُخْتَارَةِ"
},
"game_details": {
"open_download_options": "افتح خيارات التنزيل",
"download_options_zero": "لا يوجد خيار تنزيل",
"download_options_one": "{{count}} خيار تنزيل",
"download_options_other": "{{count}} خيار تنزيل",
"updated_at": "تم التحديث {{updated_at}}",
"install": "تثبيت",
"resume": "استئناف",
"pause": "إيقاف",
"cancel": "إلغاء",
"remove": "إزالة",
"space_left_on_disk": "{{space}} متبقية على القرص",
"eta": "الوقت المتبقي {{eta}}",
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
"filter": "تصفية حزم إعادة التجميع",
"requirements": "متطلبات النظام",
"minimum": "الحد الأدنى",
"recommended": "موصى به",
"release_date": "تم الإصدار في {{date}}",
"publisher": "نشر بواسطة {{publisher}}",
"hours": "ساعات",
"minutes": "دقائق",
"amount_hours": "{{amount}} ساعات",
"amount_minutes": "{{amount}} دقائق",
"accuracy": "دقة {{accuracy}}%",
"add_to_library": "إضافة إلى المكتبة",
"remove_from_library": "إزالة من المكتبة",
"no_downloads": "لا توجد تنزيلات متاحة",
"play_time": "تم اللعب لمدة {{amount}}",
"last_time_played": "آخر مرة لعبت {{period}}",
"not_played_yet": "لم تلعب {{title}} بعد",
"next_suggestion": "الاقتراح التالي",
"play": "لعب",
"deleting": "جاري حذف المثبت...",
"close": "إغلاق",
"playing_now": "قيد التشغيل الآن",
"change": "تغيير",
"repacks_modal_description": "اختر الحزمة التي تريد تنزيلها",
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى الإعدادات",
"download_now": "تنزيل الآن",
"no_shop_details": "لم يتم استرداد تفاصيل المتجر.",
"download_options": "خيارات التنزيل",
"download_path": "مسار التنزيل",
"previous_screenshot": "لقطة الشاشة السابقة",
"next_screenshot": "لقطة الشاشة التالية",
"screenshot": "لقطة شاشة {{number}}",
"open_screenshot": "افتح لقطة الشاشة {{number}}"
"open_download_options": "فَتْحُ خِيَارَاتِ التَّنْزِيلِ",
"download_options_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
"download_options_one": "{{count}} خِيَارُ تَنْزِيلٍ",
"download_options_other": "{{count}} خِيَارَاتُ تَنْزِيلٍ",
"updated_at": "تَمَّ التَّحْدِيثُ فِي {{updated_at}}",
"install": "تَثْبِيتٌ",
"resume": "اسْتِئْنَافٌ",
"pause": "إِيقَافٌ",
"cancel": "إِلْغَاءٌ",
"remove": "إِزَالَةٌ",
"space_left_on_disk": "{{space}} مُتَبَقٍّ عَلَى الْقُرْصِ",
"eta": "الِاكْتِمَالُ {{eta}}",
"calculating_eta": "جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
"filter": "تَصْفِيَةُ الْإِصْدَارَاتِ الْمُعَادِ تَغْلِيفُهَا",
"requirements": "مُتَطَلَّبَاتُ النِّظَامِ",
"minimum": "الْأَدْنَى",
"recommended": "الْمُوَصَّى بِهِ",
"paused": "مُوْقَفٌ",
"release_date": "تَمَّ الْإِصْدَارُ فِي {{date}}",
"publisher": "نُشِرَ بِوَاسِطَةِ {{publisher}}",
"hours": "سَاعَاتٌ",
"minutes": "دَقَائِقُ",
"amount_hours": "{{amount}} سَاعَاتٌ",
"amount_minutes": "{{amount}} دَقَائِقُ",
"accuracy": "دِقَّةٌ {{accuracy}}%",
"add_to_library": "إِضَافَةٌ إِلَى الْمَكْتَبَةِ",
"remove_from_library": "إِزَالَةٌ مِنَ الْمَكْتَبَةِ",
"no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ",
"play_time": "لُعِبَ لِمُدَّةِ {{amount}}",
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
"not_played_yet": "لَمْ تَلْعَبْ {{title}} بَعْدُ",
"next_suggestion": "الِاقْتِرَاحُ التَّالِي",
"play": "لَعِبٌ",
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
"close": "إِغْلَاقٌ",
"playing_now": "جَارِي اللَّعِبُ الْآن",
"change": "تَغْيِيرٌ",
"repacks_modal_description": "اخْتَرِ الْإِصْدَارَ الْمُعَادَ تَغْلِيفُهُ الَّذِي تُرِيدُ تَنْزِيلَهُ",
"select_folder_hint": "لِتَغْيِيرِ الْمَجَلَّدِ الافْتِرَاضِيِّ، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
"download_now": "تَنْزِيلٌ الْآن",
"no_shop_details": "لَمْ يَتَمَكَّنْ مِنْ اسْتِرْدَادِ تَفَاصِيلِ الْمَتْجَرِ.",
"download_options": "خِيَارَاتُ التَّنْزِيلِ",
"download_path": "مَسَارُ التَّنْزِيلِ",
"previous_screenshot": "لَقْطَةُ الشَّاشَةِ السَّابِقَةُ",
"next_screenshot": "لَقْطَةُ الشَّاشَةِ التَّالِيَةُ",
"screenshot": "لَقْطَةُ الشَّاشَةِ {{number}}",
"open_screenshot": "فَتْحُ لَقْطَةِ الشَّاشَةِ {{number}}",
"download_settings": "إعْدَادَاتُ التَّنْزِيلِ",
"downloader": "الْمُنَزِّلُ",
"select_executable": "تَحْدِيدٌ",
"no_executable_selected": "لَمْ يُحَدَّدْ مِلَفٌّ تَنْفِيذِيٌّ",
"open_folder": "فَتْحُ الْمَجَلَّدِ",
"open_download_location": "مُشَاهَدَةُ الْمَلَفَّاتِ الْمُنَزَّلَةِ",
"create_shortcut": "إِنْشَاءُ طَرِيقٍ مُخْتَصَرٍ عَلَى سَطْحِ الْمَكْتَبِ",
"clear": "مَسْحٌ",
"remove_files": "إِزَالَةُ الْمَلَفَّاتِ",
"remove_from_library_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
"remove_from_library_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ {{game}} مِنْ مَكْتَبَتِكَ",
"options": "خِيَارَاتٌ",
"executable_section_title": "الْمِلَفُّ التَّنْفِيذِيُّ",
"executable_section_description": "مَسَارُ الْمِلَفِّ الَّذِي سَيَتِمُّ تَنْفِيذُهُ عِنْدَ النَّقْرِ عَلَى \"لَعِبٌ\"",
"downloads_secion_title": "التَّنْزِيلَاتُ",
"downloads_section_description": "تَحَقَّقْ مِنَ التَّحْدِيثَاتِ أَوِ الْإِصْدَارَاتِ الْأُخْرَى لِهَذِهِ اللُّعْبَةِ",
"danger_zone_section_title": "مِنْطَقَةُ الْخَطَرِ",
"danger_zone_section_description": "إِزَالَةُ هَذِهِ اللُّعْبَةِ مِنْ مَكْتَبَتِكَ أَوِ الْمَلَفَّاتِ الْمُنَزَّلَةِ بِوَاسِطَةِ Hydra",
"download_in_progress": "جَارٍ التَّنْزِيلُ",
"download_paused": "التَّنْزِيلُ مُوْقَفٌ",
"last_downloaded_option": "خِيَارُ التَّنْزِيلِ الْأَخِيرُ",
"create_shortcut_success": "تَمَّ إِنْشَاءُ الطَّرِيقِ الْمُخْتَصَرِ بِنَجَاحٍ",
"create_shortcut_error": "خَطَأٌ فِي إِنْشَاءِ الطَّرِيقِ الْمُخْتَصَرِ",
"nsfw_content_title": "هَذِهِ اللُّعْبَةُ تَحْتَوِي عَلَى مُحْتَوًى غَيْرِ لَائِقٍ",
"nsfw_content_description": "{{title}} تَحْتَوِي عَلَى مُحْتَوًى قَدْ لَا يَكُونُ مُنَاسِبًا لِجَمِيعِ الْأَعْمَارِ. هَلْ أَنْتَ مُتَأَكِّدٌ مِنْ أَنَّكَ تُرِيدُ الْمُتَابَعَةَ؟",
"allow_nsfw_content": "الْمُتَابَعَةُ",
"refuse_nsfw_content": "الرُّجُوعُ",
"stats": "الإحْصَائِيَّاتُ",
"download_count": "التَّنْزِيلَاتُ",
"player_count": "اللَّاعِبُونَ النَّشِطُونَ",
"download_error": "هَذَا خِيَارُ التَّنْزِيلِ غَيْرُ مَتَوَفِّرٍ",
"download": "تَنْزِيلٌ",
"executable_path_in_use": "الْمِلَفُّ التَّنْفِيذِيُّ مُسْتَخْدَمٌ بِوَاسِطَةِ \"{{game}}\"",
"warning": "تَنْبِيهٌ:",
"hydra_needs_to_remain_open": "لِهَذَا التَّنْزِيلِ، يَجِبُ أَنْ يَبْقَى Hydra مَفْتُوحًا حَتَّى يَتِمَّ الِاكْتِمَالُ. إِذَا أُغْلِقَ Hydra قَبْلَ الِاكْتِمَالِ، سَتَفْقِدُ تَقَدُّمَكَ.",
"achievements": "الإِنْجَازَاتُ",
"achievements_count": "الإِنْجَازَاتُ {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "حِفْظٌ سَحَابِيٌّ",
"cloud_save_description": "احْفَظْ تَقَدُّمَكَ فِي السَّحَابَةِ وَاسْتَمِرَّ فِي اللَّعِبِ عَلَى أَيِّ جِهَازٍ",
"backups": "الْنُسَخُ الِاحْتِيَاطِيَّةُ",
"install_backup": "تَثْبِيتٌ",
"delete_backup": "حَذْفٌ",
"create_backup": "نُسْخَةٌ احْتِيَاطِيَّةٌ جَدِيدَةٌ",
"last_backup_date": "آخِرُ نُسْخَةٍ احْتِيَاطِيَّةٍ فِي {{date}}",
"no_backup_preview": "لَمْ يُعْثَرْ عَلَى أَيِّ أَلْعَابٍ مَحْفُوظَةٍ لِهَذَا الْعُنْوَانِ",
"restoring_backup": "جَارٍ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ ({{progress}} مَكْتُومٌ)...",
"uploading_backup": "جَارٍ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ...",
"no_backups": "لَمْ تَقُمْ بِإِنْشَاءِ أَيِّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ بَعْدُ",
"backup_uploaded": "تَمَّ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"backup_deleted": "تَمَّ حَذْفُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"backup_restored": "تَمَّ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"see_all_achievements": "عَرْضُ جَمِيعِ الإِنْجَازَاتِ",
"sign_in_to_see_achievements": "سَجِّلِ الدُّخُولَ لِعَرْضِ الإِنْجَازَاتِ",
"mapping_method_automatic": "آلِيٌّ",
"mapping_method_manual": "يَدَوِيٌّ",
"mapping_method_label": "طَرِيقَةُ التَّحْدِيدِ",
"files_automatically_mapped": "تَمَّ تَحْدِيدُ الْمَلَفَّاتِ تِلْقَائِيًّا",
"no_backups_created": "لَمْ تُنْشَأْ أَيُّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ",
"manage_files": "إِدَارَةُ الْمَلَفَّاتِ",
"loading_save_preview": "جَارٍ الْبَحْثُ عَنْ أَلْعَابٍ مَحْفُوظَةٍ...",
"wine_prefix": "بَادِئَةُ Wine",
"wine_prefix_description": "بَادِئَةُ Wine الْمُسْتَخْدَمَةُ لِتَشْغِيلِ هَذِهِ اللُّعْبَةِ",
"launch_options": "خِيَارَاتُ الْإِطْلَاقِ",
"launch_options_description": "يُمْكِنُ لِلْمُسْتَخْدِمِينَ الْمُتَقَدِّمِينَ إِدْخَالُ تَعْدِيلَاتٍ عَلَى خِيَارَاتِ الْإِطْلَاقِ",
"launch_options_placeholder": "لَمْ يُحَدَّدْ أَيُّ مُعَامِلٍ",
"no_download_option_info": "لَا تَوْجَدُ مَعْلُومَاتٌ مَتَوَفِّرَةٌ",
"backup_deletion_failed": "فَشَلَ فِي حَذْفِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"max_number_of_artifacts_reached": "تَمَّ بَلُوغُ الْعَدَدِ الْأَقْصَى لِلنُّسَخِ الِاحْتِيَاطِيَّةِ لِهَذِهِ اللُّعْبَةِ",
"achievements_not_sync": "تَعَرَّفْ عَلَى كَيْفِيَّةِ مَزْجِ إِنْجَازَاتِكَ",
"manage_files_description": "إِدَارَةُ الْمَلَفَّاتِ الَّتِي سَيَتِمُّ نَسْخُهَا احْتِيَاطِيًّا وَاسْتِعَادَتُهَا",
"select_folder": "تَحْدِيدُ الْمَجَلَّدِ",
"backup_from": "نُسْخَةٌ احْتِيَاطِيَّةٌ مِنْ {{date}}",
"custom_backup_location_set": "تَمَّ تَحْدِيدُ مَوْقِعِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ الْمُخَصَّصِ",
"no_directory_selected": "لَمْ يُحَدَّدْ أَيُّ دَلِيلٍ"
},
"activation": {
"title": "تفعيل هايدرا",
"installation_id": "معرف التثبيت:",
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
"message": "إذا كنت لا تعرف أين تسأل عن هذا ، فلا يجب أن يكون لديك هذا.",
"activate": "تفعيل",
"loading": "جار التحميل…"
"title": َفْعِيلُ Hydra",
"installation_id": ُعَرِّفُ التَّثْبِيتِ:",
"enter_activation_code": َدْخِلْ رَمْزَ التَّفْعِيلِ",
"message": ِذَا كُنْتَ لَا تَعْرِفُ أَيْنَ تَطْلُبُ هَذَا، فَلا يَجِبُ أَنْ تَكُونَ لَدَيْكَ.",
"activate": َفْعِيلٌ",
"loading": َارٍ التَّحْمِيلُ..."
},
"downloads": {
"resume": "استئناف",
"pause": "إيقاف مؤقت",
"eta": "الوقت المتبقي {{eta}}",
"paused": "متوقفة مؤقتًا",
"verifying": "جار التحقق…",
"completed": "اكتمل",
"cancel": "إلغاء",
"filter": "تصفية الألعاب التي تم تنزيلها",
"remove": "إزالة",
"downloading_metadata": "جار تنزيل البيانات الوصفية…",
"deleting": "جار حذف المثبت…",
"delete": "إزالة المثبت",
"delete_modal_title": "هل أنت متأكد؟",
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
"install": "تثبيت"
"resume": "اسْتِئْنَافٌ",
"pause": "إِيقَافٌ",
"eta": "الِاكْتِمَالُ {{eta}}",
"paused": "مُوْقَفٌ",
"verifying": "جَارٍ التَّحَقُّقُ...",
"completed": "مَكْتُومٌ",
"removed": "لَمْ يُنَزَّلْ",
"cancel": "إِلْغَاءٌ",
"filter": "تَصْفِيَةُ الْأَلْعَابِ الْمُنَزَّلَةِ",
"remove": "إِزَالَةٌ",
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
"delete": "حَذْفُ الْمُثَبِّتِ",
"delete_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
"delete_modal_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ جَمِيعِ مَلَفَّاتِ التَّثْبِيتِ مِنْ حَاسُوبِكَ",
"install": "تَثْبِيتٌ",
"download_in_progress": "جَارٍ التَّنْفِيذُ",
"queued_downloads": "التَّنْزِيلَاتُ فِي الْانْتِظَارِ",
"downloads_completed": "مَكْتُومٌ",
"queued": "فِي الْانْتِظَارِ",
"no_downloads_title": "فَرَاغٌ تَامٌ",
"no_downloads_description": "لَمْ تَقُمْ بِتَنْزِيلِ أَيِّ شَيْءٍ بِاسْتِخْدَامِ Hydra بَعْدُ، لَكِنَّهُ لَيْسَ مُتَأَخِّرًا لِلْبَدْءِ.",
"checking_files": "جَارٍ التَّحَقُّقُ مِنَ الْمَلَفَّاتِ...",
"seeding": "الْبَذْرُ",
"stop_seeding": "إِيقَافُ الْبَذْرِ",
"resume_seeding": "اسْتِئْنَافُ الْبَذْرِ",
"options": "إِدَارَةٌ"
},
"settings": {
"downloads_path": "مسار التنزيلات",
"change": "تحديث",
"notifications": "الإشعارات",
"enable_download_notifications": "عند اكتمال التنزيل",
"enable_repack_list_notifications": "عند إضافة حزمة جديدة",
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal-Debrid ",
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
"launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام",
"general": "عام",
"behavior": "السلوك",
"enable_real_debrid": "تفعيل Real-Debrid ",
"real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا",
"save_changes": "حفظ التغييرات"
"downloads_path": "مَسَارُ التَّنْزِيلَاتِ",
"change": "تَحْدِيثٌ",
"notifications": "الإِشْعَارَاتُ",
"enable_download_notifications": "عِنْدَ اكْتِمَالِ التَّنْزِيلِ",
"enable_repack_list_notifications": "عِنْدَ إِضَافَةِ إِصْدَارٍ مُعَادٍ تَغْلِيفِهِ جَدِيدٍ",
"real_debrid_api_token_label": "رَمْزُ واجهة برمجة التطبيقات Real-Debrid",
"quit_app_instead_hiding": "لا تُخْفِ Hydra عِنْدَ الإِغْلَاقِ",
"launch_with_system": "تَشْغِيلُ Hydra عِنْدَ بَدْءِ النِّظَامِ",
"general": "عَامٌ",
"behavior": "سُلُوكٌ",
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
"language": "اللُّغَةُ",
"real_debrid_api_token": "رَمْزُ واجهة برمجة التطبيقات",
"enable_real_debrid": "تَمْكِينُ Real-Debrid",
"real_debrid_description": "Real-Debrid هُوَ مُنَزِّلٌ غَيْرُ مَقْيُودٍ يَتِيحُ لَكَ تَنْزِيلَ الْمَلَفَّاتِ بِسُرْعَةٍ، مَحْدُودٌ فَقَطْ بِسُرْعَةِ الْإِنْتَرْنِتِ لَدَيْكَ.",
"real_debrid_invalid_token": "رَمْزُ واجهة برمجة التطبيقات غَيْرُ صَالِحٍ",
"real_debrid_api_token_hint": "يُمْكِنُكَ الْحُصُولُ عَلَى رَمْزِ واجهة برمجة التطبيقات <0>هُنَا</0>",
"real_debrid_free_account_error": "الْحِسَابُ \"{{username}}\" هُوَ حِسَابٌ مَجَّانِيٌّ. يَرْجَى الِاشْتِرَاكُ فِي Real-Debrid",
"real_debrid_linked_message": "تَمَّ رَبْطُ الْحِسَابِ \"{{username}}\"",
"save_changes": "حِفْظُ التَّغْيِيرَاتِ",
"changes_saved": "تَمَّ حِفْظُ التَّغْيِيرَاتِ بِنَجَاحٍ",
"download_sources_description": "سَتَقُومُ Hydra بِجَلْبِ رَوَابِطِ التَّنْزِيلِ مِنْ هَذِهِ الْمَصَادِرِ. يَجِبُ أَنْ يَكُونَ عُنْوَانُ URL لِلْمَصْدَرِ رَابِطًا مُبَاشِرًا إِلَى مِلَفٍّ .json يَحْتَوِي عَلَى رَوَابِطِ التَّنْزِيلِ.",
"validate_download_source": "تَصْدِيقٌ",
"remove_download_source": "إِزَالَةٌ",
"add_download_source": "إِضَافَةُ مَصْدَرٍ",
"download_count_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
"download_count_one": "{{countFormatted}} خِيَارُ تَنْزِيلٍ",
"download_count_other": "{{countFormatted}} خِيَارَاتُ تَنْزِيلٍ",
"download_source_url": "عُنْوَانُ مَصْدَرِ التَّنْزِيلِ",
"add_download_source_description": "أَدْخِلْ عُنْوَانَ URL لِمِلَفٍّ .json",
"download_source_up_to_date": "مُحَدَّثٌ",
"download_source_errored": "خَطَأٌ",
"sync_download_sources": "مَزْجُ الْمَصَادِرِ",
"removed_download_source": "تَمَّ إِزَالَةُ مَصْدَرِ التَّنْزِيلِ",
"added_download_source": "تَمَّتْ إِضَافَةُ مَصْدَرِ التَّنْزِيلِ",
"download_sources_synced": "تَمَّ مَزْجُ جَمِيعِ مَصَادِرِ التَّنْزِيلِ",
"insert_valid_json_url": "أَدْخِلْ عُنْوَانَ JSON صَالِحًا",
"found_download_option_zero": "لَمْ يُعْثَرْ عَلَى خِيَارِ تَنْزِيلٍ",
"found_download_option_one": "عُثِرَ عَلَى {{countFormatted}} خِيَارِ تَنْزِيلٍ",
"found_download_option_other": "عُثِرَ عَلَى {{countFormatted}} خِيَارَاتِ تَنْزِيلٍ",
"import": "اسْتِيرَادٌ",
"public": "عَامٌ",
"private": "خَاصٌ",
"friends_only": "الْأَصْدِقَاءُ فَقَطْ",
"privacy": "الْخُصُوصِيَّةُ",
"profile_visibility": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
"profile_visibility_description": "اخْتَرْ مَنْ يُمْكِنُهُ رُؤْيَةُ مَلَفِّكَ الشَّخْصِيِّ وَمَكْتَبَتِكَ",
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
"source_already_exists": "تَمَّتْ إِضَافَةُ هَذَا الْمَصْدَرِ مِنْ قَبْلُ",
"must_be_valid_url": "يَجِبُ أَنْ يَكُونَ الْمَصْدَرُ عُنْوَانَ URL صَالِحًا",
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
"user_unblocked": "تَمَّ إِزَالَةُ حَظْرِ الْمُسْتَخْدِمِ",
"enable_achievement_notifications": "عِنْدَ فَتْحِ إِنْجَازٍ",
"launch_minimized": "تَشْغِيلُ Hydra مُصَغَّرًا",
"disable_nsfw_alert": "تَعْطِيلُ تَنْبِيهِ الْمُحْتَوَى غَيْرِ اللَّائِقِ",
"seed_after_download_complete": "الْبَذْرُ بَعْدَ اكْتِمَالِ التَّنْزِيلِ",
"show_hidden_achievement_description": "إِظْهَارُ وَصْفِ الإِنْجَازَاتِ الْمَخْفِيَّةِ قَبْلَ فَتْحِهَا"
},
"notifications": {
"download_complete": "تم التحميل",
"game_ready_to_install": "{{title}} جاهزة للتثبيت",
"repack_list_updated": "قائمة التجميعات المحدثة",
"repack_count_one": "{{count}} حزمة مضافة",
"repack_count_other": "{{count}} حزم مُضافة"
"download_complete": "اكْتِمَالُ التَّنْزِيلِ",
"game_ready_to_install": "{{title}} جَاهِزٌ لِلتَّثْبِيتِ",
"repack_list_updated": "تَمَّ تَحْدِيثُ قَائِمَةِ الإِصْدَارَاتِ الْمُعَادَةِ تَغْلِيفُهَا",
"repack_count_one": "{{count}} إِصْدَارٌ مُعَادٌ تَغْلِيفُهُ أُضِيفَ",
"repack_count_other": "{{count}} إِصْدَارَاتٌ مُعَادَةٌ تَغْلِيفُهَا أُضِيفَتْ",
"new_update_available": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ",
"restart_to_install_update": "أَعِدْ تَشْغِيلَ Hydra لِتَثْبِيتِ التَّحْدِيثِ",
"notification_achievement_unlocked_title": "تَمَّ فَتْحُ إِنْجَازٍ لِـ {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} وَ{{count}} أُخْرَى تَمَّ فَتْحُهَا"
},
"system_tray": {
"open": "فتح هايدرا",
"quit": "خروج"
"open": َتْحُ Hydra",
"quit": "الْخُرُوجُ"
},
"game_card": {
"no_downloads": "لا توجد تنزيلات متاحة"
"no_downloads": َا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ"
},
"binary_not_found_modal": {
"title": "البرامج غير مثبتة",
"description": "لم يتم العثور على ملفات Wine أو Lutris التنفيذية على نظامك",
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
"title": "الْبَرَامِجُ غَيْرُ مُثَبَّتَةٍ",
"description": َمْ يُعْثَرْ عَلَى مَلَفَّاتٍ تَنْفِيذِيَّةٍ لِـ Wine أَوْ Lutris عَلَى نِظَامِكَ",
"instructions": َحَقَّقْ مِنَ الطَّرِيقَةِ الصَّحِيحَةِ لِتَثْبِيتِ أَيٍّ مِنْهُمَا عَلَى تَوْزِيعَةِ Linux لَدَيْكَ لِتَعْمَلَ اللُّعْبَةُ بِشَكْلٍ طَبِيعِيٍّ"
},
"modal": {
"close": "زر إغلاق"
"close": "زِرُّ الإِغْلَاقِ"
},
"forms": {
"toggle_password_visibility": "تَبْدِيلُ رُؤْيَةِ كَلِمَةِ الْمَرُورِ"
},
"user_profile": {
"amount_hours": "{{amount}} سَاعَاتٌ",
"amount_minutes": "{{amount}} دَقَائِقُ",
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
"activity": "النَّشَاطُ الْأَخِيرُ",
"library": "الْمَكْتَبَةُ",
"total_play_time": "إِجْمَالِيُّ وَقْتِ اللَّعِبِ",
"no_recent_activity_title": "هَمَمْ... لَا شَيْءَ هُنَا",
"no_recent_activity_description": "لَمْ تَلْعَبْ أَيَّ أَلْعَابٍ مُؤَخَّرًا. حَانَ الْوَقْتُ لِتَغْيِيرِ ذَلِكَ!",
"display_name": "اسْمُ الْعَرْضِ",
"saving": "جَارٍ الْحِفْظُ",
"save": "حِفْظٌ",
"edit_profile": "تَحْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
"saved_successfully": "تَمَّ الْحِفْظُ بِنَجَاحٍ",
"try_again": "الرَّجَاءُ الْمُحَاوَلَةُ مَرَّةً أُخْرَى",
"sign_out_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
"cancel": "إِلْغَاءٌ",
"successfully_signed_out": "تَمَّ تَسْجِيلُ الْخُرُوجِ بِنَجَاحٍ",
"sign_out": "تَسْجِيلُ الْخُرُوجِ",
"playing_for": "جَارِي اللَّعِبُ لِمُدَّةِ {{amount}}",
"sign_out_modal_text": "مَكْتَبَتُكَ مُرْتَبِطَةٌ بِحِسَابِكَ الْحَالِيِّ. عِنْدَ تَسْجِيلِ الْخُرُوجِ، لَنْ تَكُونَ مَكْتَبَتُكَ مَرْئِيَّةً بَعْدَ الْآنِ، وَلَنْ يَتِمَّ حِفْظُ أَيِّ تَقَدُّمٍ. هَلْ تُرِيدُ الْمُتَابَعَةَ مَعَ تَسْجِيلِ الْخُرُوجِ؟",
"add_friends": "إِضَافَةُ الْأَصْدِقَاءِ",
"add": "إِضَافَةٌ",
"friend_code": "رَمْزُ الصَّدِيقِ",
"see_profile": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
"sending": "جَارٍ الْإِرْسَالُ",
"friend_request_sent": "تَمَّ إِرْسَالُ طَلَبِ الصَّدَاقَةِ",
"friends": "الْأَصْدِقَاءُ",
"friends_list": "قَائِمَةُ الْأَصْدِقَاءِ",
"user_not_found": "الْمُسْتَخْدِمُ غَيْرُ مَوْجُودٍ",
"block_user": "حَظْرُ الْمُسْتَخْدِمِ",
"add_friend": "إِضَافَةُ صَدِيقٍ",
"request_sent": "تَمَّ إِرْسَالُ الطَّلَبِ",
"request_received": "تَمَّ اسْتِقْبَالُ الطَّلَبِ",
"accept_request": "قَبُولُ الطَّلَبِ",
"ignore_request": "تَجَاهُلُ الطَّلَبِ",
"cancel_request": "إِلْغَاءُ الطَّلَبِ",
"undo_friendship": "إِلْغَاءُ الصَّدَاقَةِ",
"request_accepted": "تَمَّ قَبُولُ الطَّلَبِ",
"user_blocked_successfully": "تَمَّ حَظْرُ الْمُسْتَخْدِمِ بِنَجَاحٍ",
"user_block_modal_text": "سَيُؤَدِّي هَذَا إِلَى حَظْرِ {{displayName}}",
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
"unblock": "إِزَالَةُ الْحَظْرِ",
"no_friends_added": "لَيْسَ لَدَيْكَ أَصْدِقَاءٌ مُضَافُونَ",
"pending": "قَيْدُ الْانْتِظَارِ",
"no_pending_invites": "لَيْسَ لَدَيْكَ دَعَوَاتٌ قَيْدُ الْانْتِظَارِ",
"no_blocked_users": "لَيْسَ لَدَيْكَ مُسْتَخْدِمُونَ مَحْظُورُونَ",
"friend_code_copied": "تَمَّ نَسْخُ رَمْزِ الصَّدِيقِ",
"undo_friendship_modal_text": "سَيُؤَدِّي هَذَا إِلَى إِلْغَاءِ صَدَاقَتِكَ مَعَ {{displayName}}",
"privacy_hint": "لِتَعْدِيلِ مَنْ يُمْكِنُهُ رُؤْيَةُ هَذَا، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
"locked_profile": "هَذَا الْمَلَفُّ الشَّخْصِيُّ خَاصٌّ",
"image_process_failure": "فَشَلَ أَثْنَاءَ مُعَالَجَةِ الصُّورَةِ",
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
"displayname_min_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَقَلِّ 3 أَحْرُفٍ",
"displayname_max_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَكْثَرِ 50 حَرْفًا",
"report_profile": "تَقْرِيرٌ عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ",
"report_reason": "لِمَاذَا تُقَدِّمُ تَقْرِيرًا عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ؟",
"report_description": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
"report_description_placeholder": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
"report": "تَقْرِيرٌ",
"report_reason_hate": "خِطَابُ الْكُرْهِ",
"report_reason_sexual_content": "مُحْتَوًى جِنْسِيٌّ",
"report_reason_violence": "عُنْفٌ",
"report_reason_spam": "رَاسِلَةٌ عَشْوَائِيَّةٌ",
"report_reason_other": "آخَرُ",
"profile_reported": "تَمَّ تَقْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
"your_friend_code": "رَمْزُ صَدِيقِكَ:",
"upload_banner": "رَفْعُ لَافِتَةٍ",
"uploading_banner": "جَارٍ رَفْعُ اللَّافِتَةِ...",
"background_image_updated": "تَمَّ تَحْدِيثُ صُورَةِ الْخَلْفِيَّةِ",
"stats": "الإحْصَائِيَّاتُ",
"achievements": "الإِنْجَازَاتُ",
"games": "الْأَلْعَابُ",
"top_percentile": "الْأَفْضَلُ {{percentile}}%",
"ranking_updated_weekly": "التَّرْتِيبُ يُحَدَّثُ أُسْبُوعِيًّا",
"playing": "جَارِي اللَّعِبُ {{game}}",
"achievements_unlocked": "الإِنْجَازَاتُ الْمَفْتُوحَةُ",
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ",
"show_achievements_on_profile": "عَرْضُ إِنْجَازَاتِكَ عَلَى مَلَفِّكَ الشَّخْصِيِّ",
"show_points_on_profile": "عَرْضُ النَّقَاطِ الْمَكْسُوبَةِ عَلَى مَلَفِّكَ الشَّخْصِيِّ"
},
"achievement": {
"achievement_unlocked": "إِنْجَازٌ مَفْتُوحٌ",
"user_achievements": "إِنْجَازَاتُ {{displayName}}",
"your_achievements": "إِنْجَازَاتُكَ",
"unlocked_at": "تَمَّ الْفَتْحُ فِي: {{date}}",
"subscription_needed": "يَحْتَاجُ اشْتِرَاكُ Hydra Cloud لِرُؤْيَةِ هَذَا الْمُحْتَوَى",
"new_achievements_unlocked": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ مِنْ {{gameCount}} أَلْعَابٍ",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إِنْجَازَاتٍ",
"achievements_unlocked_for_game": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ لِـ {{gameTitle}}",
"hidden_achievement_tooltip": "هَذَا إِنْجَازٌ مَخْفِيٌّ",
"achievement_earn_points": "اكْسِبْ {{points}} نَقَاطًا بِهَذَا الإِنْجَازِ",
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ:",
"available_points": "النَّقَاطُ الْمُتَوَفِّرَةُ:",
"how_to_earn_achievements_points": "كَيْفَ تَكْسِبُ نَقَاطَ الإِنْجَازَاتِ؟"
},
"hydra_cloud": {
"subscription_tour_title": "اشْتِرَاكُ Hydra Cloud",
"subscribe_now": "اشْتَرِكِ الْآنَ",
"cloud_saving": "حِفْظٌ سَحَابِيٌّ",
"cloud_achievements": "حِفْظُ إِنْجَازَاتِكَ فِي السَّحَابَةِ",
"animated_profile_picture": "صُورُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
"premium_support": "الدَّعْمُ الْمُتَقَدِّمُ",
"show_and_compare_achievements": "عَرْضٌ وَمُقَارَنَةُ إِنْجَازَاتِكَ مَعَ مُسْتَخْدِمِينَ آخَرِينَ",
"animated_profile_banner": "لَافِتَةُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "لَقَدْ اكْتَشَفْتَ مِيزَةً مِنْ Hydra Cloud!",
"learn_more": "تَعَلَّمْ أَكْثَرَ"
}
}

View file

@ -46,10 +46,20 @@
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
},
"catalogue": {
"next_page": "Следваща страница",
"previous_page": "Предишна страница"
"search": "Филтър…",
"developers": "Разработчици",
"genres": "Жанрове",
"tags": "Тагове",
"publishers": "Издатели",
"download_sources": "Източници за изтегляне",
"result_count": "{{resultCount}} резултати",
"filter_count": "{{filterCount}} налични",
"clear_filters": "Изчисти {{filterCount}} избрани"
},
"game_details": {
"launch_options": "Опции за стартиране",
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране (экспериментальный)",
"launch_options_placeholder": "Няма зададен параметър",
"open_download_options": "Варианти за изтегляне",
"download_options_zero": "Няма варианти за изтегляне",
"download_options_one": "{{count}} варианти за изтегляне",
@ -177,6 +187,10 @@
"loading": "Зареждане…"
},
"downloads": {
"seeding": "Сийдване",
"stop_seeding": "Спри сийдването",
"resume_seeding": "Продължи сийдването",
"options": "Управление",
"resume": "Продължи",
"pause": "Пауза",
"eta": "Conclusion {{eta}}",
@ -202,6 +216,8 @@
"checking_files": "Проверка на файлове…"
},
"settings": {
"seed_after_download_complete": "Сийд след завършване на изтеглянето",
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключването им",
"downloads_path": "Инсталационен път",
"change": "Актуализиране",
"notifications": "Известия",
@ -210,7 +226,7 @@
"real_debrid_api_token_label": "Real-Debrid API токен",
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
"general": "Общ",
"general": "Общи",
"behavior": "Поведение",
"download_sources": "Източници за изтегляне",
"language": "Език",
@ -288,12 +304,22 @@
"toggle_password_visibility": "Превключване на видимостта на паролата"
},
"user_profile": {
"stats": "Статистики",
"achievements": "Постижения",
"games": "Игри",
"top_percentile": "Топ {{percentile}}%",
"ranking_updated_weekly": "Класацията се актуализира седмично",
"playing": "Играе {{game}}",
"achievements_unlocked": "Отключени постижения",
"earned_points": "Спечелени точки",
"show_achievements_on_profile": "Показвай своите постижения в профила",
"show_points_on_profile": "Показвай спечелените точки в профила",
"amount_hours": "{{amount}} часове",
"amount_minutes": "{{amount}} минути",
"last_time_played": "Последно играно {{period}}",
"activity": "Скорошна активност",
"library": "Библиотека",
"total_play_time": "Общо време за игра: {{amount}}",
"total_play_time": "Общо време за игра",
"no_recent_activity_title": "Хмм… няма нищо тук",
"no_recent_activity_description": "Не сте играли игри напоследък. Време е да промените това.!",
"display_name": "Показване на името",
@ -359,16 +385,24 @@
"background_image_updated": "Обновено фоново изображение"
},
"achievement": {
"hidden_achievement_tooltip": "Това е скрито постижение",
"achievement_earn_points": "Спечели {{points}} точки с това постижение",
"earned_points": "Спечелени точки:",
"available_points": "Налични точки:",
"how_to_earn_achievements_points": "Как да спечелиш точки за постижения?",
"achievement_unlocked": "Постижението е отключено",
"user_achievements": "Постиженията на {{displayName}} ",
"your_achievements": "Вашите Постижения",
"unlocked_at": "Отключено на:",
"unlocked_at": "Отключено на: {{date}}",
"subscription_needed": "Необходим е абонамент за Hydra Cloud, за да видите това съдържание",
"new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игра",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения",
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
},
"tour": {
"hydra_cloud": {
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Открихте функция на Hydra Cloud!",
"learn_more": "Научете повече",
"subscription_tour_title": "Hydra Cloud Абонамент",
"subscribe_now": "Абонирай се сега",
"cloud_saving": "Запазване в облака",

View file

@ -224,7 +224,7 @@
"last_time_played": "Última partida {{period}}",
"activity": "Activitat recent",
"library": "Biblioteca",
"total_play_time": "Temps total de joc:{{amount}}",
"total_play_time": "Temps total de joc",
"no_recent_activity_title": "Hmmm… encara no res",
"no_recent_activity_description": "No has jugat a cap joc recentment. És el moment de canviar-ho!",
"display_name": "Nom de visualització",

View file

@ -293,7 +293,7 @@
"last_time_played": "Naposledy hráno {{period}}",
"activity": "Nedávná aktivita",
"library": "Knihovna",
"total_play_time": "Celkový odehraný čas: {{amount}}",
"total_play_time": "Celkový odehraný čas",
"no_recent_activity_title": "Hmmm… nic tu není",
"no_recent_activity_description": "V poslední době si nehrál žádnout hru, můžeš to ale napravit!",
"display_name": "Zobrazované jméno",
@ -362,13 +362,13 @@
"achievement_unlocked": "Achievement odemčen",
"user_achievements": "Achievementy uživatele {{displayName}}",
"your_achievements": "Vaše achievementy",
"unlocked_at": "Odemčeno:",
"unlocked_at": "Odemčeno: {{date}}",
"subscription_needed": "Je vyžadováno předplatné Hydra Cloud pro zobrazení tohoto obsahu",
"new_achievements_unlocked": "Odemčeno {{achievementCount}} nových achievementů z {{gameCount}} her",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementů",
"achievements_unlocked_for_game": "Odemčeno {{achievementCount}} nových achievementů pro {{gameTitle}}"
},
"tour": {
"hydra_cloud": {
"subscription_tour_title": "Předplatné Hydra Cloud",
"subscribe_now": "Připojit se",
"cloud_saving": "Ukládání v cloudu",

View file

@ -251,7 +251,7 @@
"last_time_played": "Sidst spillet {{period}}",
"activity": "Seneste aktivitet",
"library": "Bibliotek",
"total_play_time": "Samlet spiltid: {{amount}}",
"total_play_time": "Samlet spiltid",
"no_recent_activity_title": "Hmmm… ikke noget her",
"no_recent_activity_description": "Du har ikke spillet nogen spil for nyligt. Dét er det på tide at lave om på!",
"display_name": "Brugernavn",

View file

@ -224,7 +224,7 @@
"last_time_played": "Zuletzt gespielt {{period}}",
"activity": "Letzte Aktivität",
"library": "Bibliothek",
"total_play_time": "Gesamtspielzeit: {{amount}}",
"total_play_time": "Gesamtspielzeit",
"no_recent_activity_title": "Hmmm… hier ist nichts",
"no_recent_activity_description": "Du hast in letzter Zeit keine Spiele gespielt. Es wird Zeit das zu ändern!",
"display_name": "Anzeigename",

View file

@ -46,8 +46,15 @@
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
},
"catalogue": {
"next_page": "Next page",
"previous_page": "Previous page"
"search": "Filter…",
"developers": "Developers",
"genres": "Genres",
"tags": "Tags",
"publishers": "Publishers",
"download_sources": "Download sources",
"result_count": "{{resultCount}} results",
"filter_count": "{{filterCount}} available",
"clear_filters": "Clear {{filterCount}} selected"
},
"game_details": {
"open_download_options": "Open download options",
@ -160,15 +167,24 @@
"loading_save_preview": "Searching for save games…",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "The Wine prefix used to run this game",
"launch_options": "Launch Options",
"launch_options_description": "Advanced users may choose to enter modifications to their launch options (experimental feature)",
"launch_options_placeholder": "No parameter specified",
"no_download_option_info": "No information available",
"backup_deletion_failed": "Failed to delete backup",
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
"achievements_not_sync": "Your achievements are not synchronized",
"achievements_not_sync": "See how to synchronize your achievements",
"manage_files_description": "Manage which files will be backed up and restored",
"select_folder": "Select folder",
"backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected"
"no_directory_selected": "No directory selected",
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
"reset_achievements": "Reset achievements",
"reset_achievements_description": "This will reset all achievements for {{game}}",
"reset_achievements_title": "Are you sure?",
"reset_achievements_success": "Achievements successfully reset",
"reset_achievements_error": "Failed to reset achievements"
},
"activation": {
"title": "Activate Hydra",
@ -201,7 +217,11 @@
"queued": "Queued",
"no_downloads_title": "Such empty",
"no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start.",
"checking_files": "Checking files…"
"checking_files": "Checking files…",
"seeding": "Seeding",
"stop_seeding": "Stop seeding",
"resume_seeding": "Resume seeding",
"options": "Manage"
},
"settings": {
"downloads_path": "Downloads path",
@ -258,7 +278,9 @@
"user_unblocked": "User has been unblocked",
"enable_achievement_notifications": "When an achievement is unlocked",
"launch_minimized": "Launch Hydra minimized",
"disable_nsfw_alert": "Disable NSFW alert"
"disable_nsfw_alert": "Disable NSFW alert",
"seed_after_download_complete": "Seed after download complete",
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them"
},
"notifications": {
"download_complete": "Download complete",
@ -295,7 +317,7 @@
"last_time_played": "Last played {{period}}",
"activity": "Recent Activity",
"library": "Library",
"total_play_time": "Total playtime: {{amount}}",
"total_play_time": "Total playtime",
"no_recent_activity_title": "Hmmm… nothing here",
"no_recent_activity_description": "You haven't played any games recently. It's time to change that!",
"display_name": "Display name",
@ -358,19 +380,34 @@
"your_friend_code": "Your friend code:",
"upload_banner": "Upload banner",
"uploading_banner": "Uploading banner…",
"background_image_updated": "Background image updated"
"background_image_updated": "Background image updated",
"stats": "Stats",
"achievements": "achievements",
"games": "Games",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Ranking is updated weekly",
"playing": "Playing {{game}}",
"achievements_unlocked": "Achievements Unlocked",
"earned_points": "Earned points",
"show_achievements_on_profile": "Show your achievements on your profile",
"show_points_on_profile": "Show your earned points on your profile"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",
"user_achievements": "{{displayName}}'s Achievements",
"your_achievements": "Your Achievements",
"unlocked_at": "Unlocked at:",
"unlocked_at": "Unlocked at: {{date}}",
"subscription_needed": "A Hydra Cloud subscription is required to see this content",
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievements",
"achievements_unlocked_for_game": "Unlocked {{achievementCount}} new achievements for {{gameTitle}}"
"achievements_unlocked_for_game": "Unlocked {{achievementCount}} new achievements for {{gameTitle}}",
"hidden_achievement_tooltip": "This is a hidden achievement",
"achievement_earn_points": "Earn {{points}} points with this achievement",
"earned_points": "Earned points:",
"available_points": "Available points:",
"how_to_earn_achievements_points": "How to earn achievements points?"
},
"tour": {
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Subscription",
"subscribe_now": "Subscribe now",
"cloud_saving": "Cloud saving",
@ -378,6 +415,9 @@
"animated_profile_picture": "Animated profile pictures",
"premium_support": "Premium Support",
"show_and_compare_achievements": "Show and compare your achievements to other users",
"animated_profile_banner": "Animated profile banner"
"animated_profile_banner": "Animated profile banner",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "You've just discovered a Hydra Cloud feature!",
"learn_more": "Learn More"
}
}

View file

@ -46,8 +46,15 @@
"checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)"
},
"catalogue": {
"next_page": "Siguiente página",
"previous_page": "Pagina anterior"
"search": "Filtrar…",
"developers": "Desarrolladores",
"genres": "Géneros",
"tags": "Marcadores",
"publishers": "Editores",
"download_sources": "Fuentes de descarga",
"result_count": "{{resultCount}} resultados",
"filter_count": "{{filterCount}} disponibles",
"clear_filters": "Limpiar {{filterCount}} seleccionados"
},
"game_details": {
"open_download_options": "Ver opciones de descargas",
@ -79,7 +86,7 @@
"add_to_library": "Agregar a la biblioteca",
"remove_from_library": "Eliminar de la biblioteca",
"no_downloads": "No hay descargas disponibles",
"play_time": "Jugado por {{amount}}",
"play_time": "Has jugado {{amount}}",
"last_time_played": "Jugado por última vez: {{period}}",
"not_played_yet": "Aún no has jugado a {{title}}",
"next_suggestion": "Siguiente sugerencia",
@ -168,7 +175,7 @@
"backup_from": "Copia de seguridad de {{date}}",
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
"clear": "Limpiar",
"no_directory_selected": "No se seleccionó un directório"
"no_directory_selected": "No se seleccionó un directorio"
},
"activation": {
"title": "Activar Hydra",
@ -201,7 +208,11 @@
"queued": "En cola",
"no_downloads_title": "Esto está tan... vacío",
"no_downloads_description": "No has descargado nada con Hydra... aún, ¡pero nunca es tarde para comenzar!.",
"checking_files": "Verificando archivos…"
"checking_files": "Verificando archivos…",
"seeding": "Seeding",
"stop_seeding": "Detener seeding",
"resume_seeding": "Continuar seeding",
"options": "Gestionar"
},
"settings": {
"downloads_path": "Ruta de descarga",
@ -258,7 +269,9 @@
"user_unblocked": "El usuario ha sido desbloqueado",
"enable_achievement_notifications": "Cuando un logro se desbloquea",
"launch_minimized": "Iniciar Hydra minimizado",
"disable_nsfw_alert": "Desactivar alerta NSFW"
"disable_nsfw_alert": "Desactivar alerta NSFW",
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos"
},
"notifications": {
"download_complete": "Descarga completada",
@ -295,7 +308,7 @@
"last_time_played": "Última vez jugado: {{period}}",
"activity": "Actividad reciente",
"library": "Biblioteca",
"total_play_time": "Total de tiempo jugado: {{amount}}",
"total_play_time": "Has jugado",
"no_recent_activity_title": "Que raro, no hay nada por acá...",
"no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!",
"display_name": "Nombre en pantalla",
@ -308,7 +321,7 @@
"cancel": "Cancelar",
"successfully_signed_out": "Sesión cerrada exitosamente",
"sign_out": "Cerrar sesión",
"playing_for": "Jugando por {{amount}}",
"playing_for": "Llevas jugando {{amount}}",
"sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?",
"add_friends": "Añadir amigos",
"add": "Añadir",
@ -358,19 +371,34 @@
"your_friend_code": "Tu código de amigo:",
"upload_banner": "Subir un banner",
"uploading_banner": "Subiendo banner…",
"background_image_updated": "Imagen de fondo actualizada"
"background_image_updated": "Imagen de fondo actualizada",
"playing": "Jugando {{game}}",
"achievements": "logros",
"achievements_unlocked": "Logros desbloqueados",
"earned_points": "Puntos Obtenidos",
"show_achievements_on_profile": "Mostrar tus logros en tu perfil",
"show_points_on_profile": "Mostrar tus puntos obtenidos en tu perfil",
"games": "Juegos",
"ranking_updated_weekly": "El Ranking se actualiza semanalmente",
"stats": "Estadísticas",
"top_percentile": "Top {{percentile}}%"
},
"achievement": {
"achievement_unlocked": "Logro desbloqueado",
"user_achievements": "Logros de {{displayName}}",
"your_achievements": "Tus Logros",
"unlocked_at": "Desbloqueado el:",
"unlocked_at": "Desbloqueado el: {{date}}",
"subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido",
"new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} logros",
"achievements_unlocked_for_game": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}"
"achievements_unlocked_for_game": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}",
"hidden_achievement_tooltip": "Este es un logro oculto",
"achievement_earn_points": "Obtén {{points}} puntos con este logro",
"earned_points": "Puntos obtenidos:",
"available_points": "Puntos disponibles:",
"how_to_earn_achievements_points": "¿Cómo obtener puntos de logros?"
},
"tour": {
"hydra_cloud": {
"subscription_tour_title": "Suscripción Hydra Cloud",
"subscribe_now": "Suscribirse ahora",
"cloud_saving": "Guardado en la nube",
@ -378,6 +406,9 @@
"animated_profile_picture": "Fotos de perfil animadas",
"premium_support": "Soporte Premium",
"show_and_compare_achievements": "Muestra y compara tus logros con otros usuarios",
"animated_profile_banner": "Fondo de perfil animado"
"animated_profile_banner": "Fondo de perfil animado",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "¡Has descubierto una característica de Hydra Cloud!",
"learn_more": "Aprender más"
}
}

View file

@ -290,7 +290,7 @@
"last_time_played": "Viimati mängitud {{period}}",
"activity": "Hiljutine aktiivsus",
"library": "Kogu",
"total_play_time": "Kogu mängitud aeg: {{amount}}",
"total_play_time": "Kogu mängitud aeg",
"no_recent_activity_title": "Hmmm… siin pole midagi",
"no_recent_activity_description": "Sa pole hiljuti ühtegi mängu mänginud. On aeg seda muuta!",
"display_name": "Kuvatav nimi",
@ -359,11 +359,11 @@
"achievement_unlocked": "Saavutus avatud",
"user_achievements": "{{displayName}} saavutused",
"your_achievements": "Sinu saavutused",
"unlocked_at": "Avatud:",
"unlocked_at": "Avatud: {{date}}",
"subscription_needed": "Selle sisu nägemiseks on vaja Hydra Cloud tellimust",
"new_achievements_unlocked": "Avatud {{achievementCount}} uut saavutust {{gameCount}} mängust"
},
"tour": {
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Tellimus",
"subscribe_now": "Telli kohe",
"cloud_saving": "Pilvesalvestus",

View file

@ -224,7 +224,7 @@
"last_time_played": "Terakhir dimainkan {{period}}",
"activity": "Aktivitas terbaru",
"library": "Perpustakaan",
"total_play_time": "Total waktu bermain: {{amount}}",
"total_play_time": "Total waktu bermain",
"no_recent_activity_title": "Hmm… kosong di sini",
"no_recent_activity_description": "Kamu belum main game baru-baru ini. Yuk, mulai main!",
"display_name": "Nama tampilan",

View file

@ -220,7 +220,7 @@
"last_time_played": "Соңғы ойын {{period}}",
"activity": "Соңғы әрекет",
"library": "Кітапхана",
"total_play_time": "Барлығы ойнаған: {{amount}}",
"total_play_time": "Барлығы ойнаған",
"no_recent_activity_title": "Хммм... Мұнда ештеңе жоқ",
"no_recent_activity_description": "Сіз ұзақ уақыт бойы ештеңе ойнаған жоқсыз. Мұны өзгерту керек!",
"display_name": "Көрсету аты",

View file

@ -251,7 +251,7 @@
"last_time_played": "Sist spilt {{period}}",
"activity": "Seneste aktivitet",
"library": "Bibliotek",
"total_play_time": "Samlet spilltid: {{amount}}",
"total_play_time": "Samlet spilltid",
"no_recent_activity_title": "Hmmm… ikke noe her",
"no_recent_activity_description": "Du har ikke spilt noen spill for på det seneste. Det er det på tide at endre på!",
"display_name": "Brukernavn",

View file

@ -155,16 +155,24 @@
"loading_save_preview": "Buscando por arquivos de salvamento…",
"wine_prefix": "Prefixo Wine",
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
"launch_options": "Opções de Inicialização",
"launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo (experimental)",
"launch_options_placeholder": "Nenhum parâmetro informado",
"no_download_option_info": "Sem informações disponíveis",
"backup_deletion_failed": "Falha ao apagar backup",
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
"achievements_not_sync": "Suas conquistas não estão sincronizadas",
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
"backup_from": "Backup de {{date}}",
"custom_backup_location_set": "Localização customizada selecionada",
"select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado"
"no_directory_selected": "Nenhum diretório selecionado",
"reset_achievements": "Resetar conquistas",
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
"reset_achievements_title": "Tem certeza?",
"reset_achievements_success": "Conquistas resetadas com sucesso",
"reset_achievements_error": "Falha ao resetar conquistas"
},
"activation": {
"title": "Ativação",
@ -197,7 +205,11 @@
"queued": "Na fila",
"no_downloads_title": "Nada por aqui…",
"no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar.",
"checking_files": "Verificando arquivos…"
"checking_files": "Verificando arquivos…",
"seeding": "Semeando",
"stop_seeding": "Parar de semear",
"resume_seeding": "Semear",
"options": "Gerenciar"
},
"settings": {
"downloads_path": "Diretório dos downloads",
@ -254,7 +266,9 @@
"user_unblocked": "Usuário desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado"
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download",
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las"
},
"notifications": {
"download_complete": "Download concluído",
@ -278,8 +292,15 @@
"instructions": "Verifique a forma correta de instalar algum deles no seu distro Linux, garantindo assim a execução normal do jogo"
},
"catalogue": {
"next_page": "Próxima página",
"previous_page": "Página anterior"
"search": "Filtrar…",
"developers": "Desenvolvedores",
"genres": "Gêneros",
"tags": "Marcadores",
"publishers": "Distribuidoras",
"download_sources": "Fontes de download",
"result_count": "{{resultCount}} resultados",
"filter_count": "{{filterCount}} disponíveis",
"clear_filters": "Limpar {{filterCount}} selecionados"
},
"modal": {
"close": "Botão de fechar"
@ -293,7 +314,7 @@
"last_time_played": "Última sessão {{period}}",
"activity": "Atividades recentes",
"library": "Biblioteca",
"total_play_time": "Tempo total de jogo: {{amount}}",
"total_play_time": "Tempo total de jogo",
"no_recent_activity_title": "Hmmm… nada por aqui",
"no_recent_activity_description": "Parece que você não jogou nada recentemente. Que tal começar agora?",
"display_name": "Nome de exibição",
@ -356,26 +377,43 @@
"your_friend_code": "Seu código de amigo:",
"upload_banner": "Carregar banner",
"uploading_banner": "Carregando banner…",
"background_image_updated": "Imagem de fundo salva"
"background_image_updated": "Imagem de fundo salva",
"stats": "Estatísticas",
"achievements": "conquistas",
"games": "Jogos",
"ranking_updated_weekly": "O ranking é atualizado semanalmente",
"playing": "Jogando {{game}}",
"achievements_unlocked": "Conquistas desbloqueadas",
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
"your_achievements": "Suas Conquistas",
"user_achievements": "Conquistas de {{displayName}}",
"unlocked_at": "Desbloqueado em:",
"unlocked_at": "Desbloqueada em: {{date}}",
"subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo",
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}"
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
"hidden_achievement_tooltip": "Está é uma conquista oculta",
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
"earned_points": "Pontos ganhos:",
"available_points": "Pontos disponíveis:",
"how_to_earn_achievements_points": "Como desbloquear pontos nas conquistas?"
},
"tour": {
"hydra_cloud": {
"subscription_tour_title": "Assinatura Hydra Cloud",
"hydra_cloud": "Hydra Cloud",
"subscribe_now": "Inscreva-se agora",
"cloud_achievements": "Salvamento de conquistas em nuvem",
"animated_profile_picture": "Fotos de perfil animadas",
"premium_support": "Suporte Premium",
"show_and_compare_achievements": "Exiba e compare suas conquistas com outros usuários",
"animated_profile_banner": "Banner animado no perfil",
"cloud_saving": "Saves de jogos em nuvem"
"cloud_saving": "Saves de jogos em nuvem",
"hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!",
"learn_more": "Saiba mais"
}
}

View file

@ -287,7 +287,7 @@
"last_time_played": "Última sessão {{period}}",
"activity": "Atividade recente",
"library": "Biblioteca",
"total_play_time": "Tempo total de jogo: {{amount}}",
"total_play_time": "Tempo total de jogo",
"no_recent_activity_title": "Hmmm… não há nada por aqui",
"no_recent_activity_description": "Parece que não jogaste nada recentemente. Que tal começar agora?",
"display_name": "Nome de apresentação",
@ -356,11 +356,11 @@
"achievement_unlocked": "Conquista desbloqueada",
"your_achievements": "As tuas Conquistas",
"user_achievements": "Conquistas de {{displayName}}",
"unlocked_at": "Desbloqueada em:",
"unlocked_at": "Desbloqueada em: {{date}}",
"subscription_needed": "Precisas de uma subscrição Hydra Cloud para visualizar este conteúdo",
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
},
"tour": {
"hydra_cloud": {
"subscription_tour_title": "Subscrição Hydra Cloud",
"subscribe_now": "Subscreve agora",
"cloud_achievements": "Gravação de conquistas na nuvem",

View file

@ -4,12 +4,13 @@
"successfully_signed_in": "Успешный вход"
},
"home": {
"featured": "Рекомендованное",
"featured": "Рекомендации",
"surprise_me": "Удиви меня",
"no_results": "Ничего не найдено",
"hot": "Сейчас в топе",
"start_typing": "Начинаю вводить текст для поиска...",
"weekly": "📅 Лучшие игры недели"
"weekly": "📅 Лучшие игры недели",
"achievements": "🏆 Игры, в которых нужно победить"
},
"sidebar": {
"catalogue": "Каталог",
@ -19,7 +20,7 @@
"downloading_metadata": "{{title}} (Загрузка метаданных…)",
"paused": "{{title}} (Приостановлено)",
"downloading": "{{title}} ({{percentage}} - Загрузка…)",
"filter": "Фильтр библиотеки",
"filter": "Поиск",
"home": "Главная",
"queued": "{{title}} (В очереди)",
"game_has_no_executable": "Файл запуска игры не выбран",
@ -45,14 +46,21 @@
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)"
},
"catalogue": {
"next_page": "Следующая страница",
"previous_page": "Предыдущая страница"
"search": "Фильтр…",
"developers": "Разработчики",
"genres": "Жанры",
"tags": "Маркеры",
"publishers": "Издательства",
"download_sources": "Источники загрузки",
"result_count": "{{resultCount}} результатов",
"filter_count": "{{filterCount}} доступных",
"clear_filters": "Очистить {{filterCount}} выбранных"
},
"game_details": {
"open_download_options": "Открыть варианты загрузки",
"download_options_zero": "Нет вариантов загрузки",
"download_options_one": "{{count}} вариант загрузки",
"download_options_other": "{{count}} вариантов загрузки",
"open_download_options": "Открыть источники",
"download_options_zero": "Нет источников",
"download_options_one": "{{count}} источник",
"download_options_other": "{{count}} источников",
"updated_at": "Обновлено {{updated_at}}",
"install": "Установить",
"resume": "Возобновить",
@ -63,7 +71,7 @@
"eta": "Окончание {{eta}}",
"calculating_eta": "Подсчёт оставшегося времени…",
"downloading_metadata": "Загрузка метаданных…",
"filter": "Фильтр репаков",
"filter": "Поиск репаков",
"requirements": "Системные требования",
"minimum": "Минимальные",
"recommended": "Рекомендуемые",
@ -77,7 +85,7 @@
"accuracy": "точность {{accuracy}}%",
"add_to_library": "Добавить в библиотеку",
"remove_from_library": "Удалить из библиотеки",
"no_downloads": "Нет доступных загрузок",
"no_downloads": "Нет доступных источников",
"play_time": "Сыграно {{amount}}",
"last_time_played": "Последний запуск {{period}}",
"not_played_yet": "Вы ещё не играли в {{title}}",
@ -91,7 +99,7 @@
"select_folder_hint": "Чтобы изменить папку загрузок по умолчанию, откройте <0>Настройки</0>",
"download_now": "Загрузить сейчас",
"no_shop_details": "Не удалось получить описание",
"download_options": "Вариантов загрузки",
"download_options": "Источники",
"download_path": "Путь для загрузок",
"previous_screenshot": "Предыдущий скриншот",
"next_screenshot": "Следующий скриншот",
@ -104,6 +112,7 @@
"open_folder": "Открыть папку",
"open_download_location": "Просмотреть папку загрузок",
"create_shortcut": "Создать ярлык на рабочем столе",
"clear": "Очистить",
"remove_files": "Удалить файлы",
"remove_from_library_title": "Вы уверены?",
"remove_from_library_description": "{{game}} будет удалена из вашей библиотеки.",
@ -113,22 +122,67 @@
"downloads_secion_title": "Загрузки",
"downloads_section_description": "Проверить наличие обновлений или других версий игры",
"danger_zone_section_title": "Опасная зона",
"danger_zone_section_description": "Удалить эту игру из вашей библиотеки или файлы скачанные Hydra",
"danger_zone_section_description": "Вы можете удалить эту игру из вашей библиотеки или файлы скачанные из Hydra",
"download_in_progress": "Идёт загрузка",
"download_paused": "Загрузка приостановлена",
"last_downloaded_option": "Последний вариант загрузки",
"create_shortcut_success": "Ярлык создан",
"create_shortcut_error": "Не удалось создать ярлык",
"allow_nsfw_content": "Продолжать",
"allow_nsfw_content": "Продолжить",
"download": "Скачать",
"download_count": "Загрузки",
"download_error": "Этот вариант загрузки недоступен",
"executable_path_in_use": "Исполняемый файл уже используется \"{{game}}\"",
"nsfw_content_description": "{{title}} содержит контент, который может не подходить для всех возрастов. \nВы уверены, что хотите продолжить?",
"nsfw_content_title": "Эта игра содержит неприемлемый контент",
"refuse_nsfw_content": "Назад",
"stats": "Статистика",
"player_count": "Активные игроки",
"refuse_nsfw_content": "Возвращаться",
"stats": "Статистика"
"warning": "Внимание:",
"hydra_needs_to_remain_open": "Для этой загрузки Hydra должна оставаться открытой до завершения. Если Hydra закроется до завершения, вы потеряете прогресс.",
"achievements": "Достижения",
"achievements_count": "Достижения {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Облачное сохранение",
"cloud_save_description": "Сохраняйте ваш прогресс в облаке и продолжайте играть на любом устройстве",
"backups": "Резервные копии",
"install_backup": "Установить",
"delete_backup": "Удалить",
"create_backup": "Создать новую резервную копию",
"last_backup_date": "Последняя резервная копия от {{date}}",
"no_backup_preview": "Сохранения для этого заголовка не найдены",
"restoring_backup": "Восстановление резервной копии ({{progress}} завершено)…",
"uploading_backup": "Загрузка резервной копии…",
"no_backups": "Вы еще не создали резервных копий для этой игры",
"backup_uploaded": "Резервная копия загружена",
"backup_deleted": "Резервная копия удалена",
"backup_restored": "Резервная копия восстановлена",
"see_all_achievements": "Просмотреть все достижения",
"sign_in_to_see_achievements": "Войдите, чтобы увидеть достижения",
"mapping_method_automatic": "Автоматическая",
"mapping_method_manual": "Ручная",
"mapping_method_label": "Метод сопоставления",
"files_automatically_mapped": "Файлы автоматически сопоставлены",
"no_backups_created": "Для этой игры не создано резервных копий",
"manage_files": "Управление файлами",
"loading_save_preview": "Поиск сохранений…",
"wine_prefix": "Префикс Wine",
"wine_prefix_description": "Префикс Wine, используемый для запуска этой игры",
"launch_options": "Параметры запуска",
"launch_options_description": "Опытные пользователи могут внести изменения в параметры запуска",
"launch_options_placeholder": "Параметр не указан ",
"no_download_option_info": "Информация недоступна",
"backup_deletion_failed": "Не удалось удалить резервную копию",
"max_number_of_artifacts_reached": "Достигнуто максимальное количество резервных копий для этой игры",
"achievements_not_sync": "Ваши достижения не синхронизированы",
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
"select_folder": "Выбрать папку",
"backup_from": "Резервная копия от {{date}}",
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
"no_directory_selected": "Не выбран каталог",
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
"reset_achievements_title": "Вы уверены?",
"reset_achievements_success": "Достижения успешно сброшены",
"reset_achievements_error": "Не удалось сбросить достижения"
},
"activation": {
"title": "Активировать Hydra",
@ -147,7 +201,7 @@
"completed": "Завершено",
"removed": "Не скачано",
"cancel": "Отмена",
"filter": "Фильтр загруженных игр",
"filter": "Поиск загруженных игр",
"remove": "Удалить",
"downloading_metadata": "Загрузка метаданных…",
"deleting": "Удаление установщика…",
@ -161,17 +215,24 @@
"queued": "В очереди",
"no_downloads_title": "Здесь так пусто...",
"no_downloads_description": "Вы ещё ничего не скачали через Hydra, но никогда не поздно начать.",
"checking_files": "Проверка файлов…"
"checking_files": "Проверка файлов…",
"seeding": "Раздача",
"stop_seeding": "Остановить раздачу",
"resume_seeding": "Продолжить раздачу",
"options": "Управлять"
},
"settings": {
"downloads_path": "Путь загрузок",
"change": "Изменить",
"notifications": "Уведомления",
"enable_download_notifications": "По завершении загрузки",
"enable_achievement_notifications": "Когда достижение разблокировано",
"enable_repack_list_notifications": "При добавлении нового репака",
"real_debrid_api_token_label": "Real-Debrid API-токен",
"quit_app_instead_hiding": "Закрывать приложение вместо сворачивания в трей",
"launch_with_system": "Запускать Hydra вместе с системой",
"launch_minimized": "Запустить Hydra в свернутом виде",
"disable_nsfw_alert": "Отключить предупреждение о непристойном контенте",
"general": "Основные",
"behavior": "Поведение",
"download_sources": "Источники загрузки",
@ -196,7 +257,7 @@
"add_download_source_description": "Вставьте ссылку на .json-файл",
"download_source_up_to_date": "Обновлён",
"download_source_errored": "Ошибка",
"sync_download_sources": "Синхронизировать источники",
"sync_download_sources": "Обновить источники",
"removed_download_source": "Источник загрузок удален",
"added_download_source": "Источник загрузок добавлен",
"download_sources_synced": "Все источники загрузок синхронизированы",
@ -206,16 +267,18 @@
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
"import": "Импортировать",
"blocked_users": "Заблокированные пользователи",
"friends_only": "Только друзья",
"friends_only": "Только для друзей",
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
"privacy": "Конфиденциальность",
"private": "Частный",
"profile_visibility": "Видимость профиля",
"profile_visibility_description": "Выберите, кто может видеть ваш профиль и библиотеку",
"public": "Общественный",
"public": "Публичный",
"required_field": "Это поле обязательно к заполнению",
"source_already_exists": "Этот источник уже добавлен",
"user_unblocked": "Пользователь разблокирован"
"user_unblocked": "Пользователь разблокирован",
"seed_after_download_complete": "Раздавать после завершения загрузки",
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением"
},
"notifications": {
"download_complete": "Загрузка завершена",
@ -223,15 +286,17 @@
"repack_list_updated": "Список репаков обновлен",
"repack_count_one": "{{count}} репак добавлен",
"repack_count_other": "{{count}} репаков добавлено",
"new_update_available": "Доступна версия {{version}}",
"restart_to_install_update": "Перезапустите Hydra для установки обновления"
"new_update_available": "Доступна новая версия {{version}}",
"restart_to_install_update": "Перезапустите Hydra для установки обновления",
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}"
},
"system_tray": {
"open": "Открыть Hydra",
"quit": "Выйти"
},
"game_card": {
"no_downloads": "Нет доступных загрузок"
"no_downloads": "Нет доступных источников"
},
"binary_not_found_modal": {
"title": "Программы не установлены",
@ -250,7 +315,7 @@
"last_time_played": "Последняя игра {{period}}",
"activity": "Недавняя активность",
"library": "Библиотека",
"total_play_time": "Всего сыграно: {{amount}}",
"total_play_time": "Всего сыграно",
"no_recent_activity_title": "Хммм... Тут ничего нет",
"no_recent_activity_description": "Вы давно ни во что не играли. Пора это изменить!",
"display_name": "Отображаемое имя",
@ -309,6 +374,46 @@
"report_reason_spam": "Спам",
"report_reason_violence": "Насилие",
"required_field": "Это поле обязательно к заполнению",
"undo_friendship_modal_text": "Это отменит вашу дружбу с {{displayName}}."
"undo_friendship_modal_text": "Это отменит вашу дружбу с {{displayName}}.",
"your_friend_code": "Код вашего друга:",
"upload_banner": "Загрузить баннер",
"uploading_banner": "Загрузка баннера...",
"background_image_updated": "Фоновое изображение обновлено",
"stats": "Статистика",
"games": "Игры",
"top_percentile": "Топ {{percentile}}%",
"ranking_updated_weekly": "Рейтинг обновляется еженедельно",
"playing": "Играет в {{game}}",
"achievements_unlocked": "Достижения разблокированы",
"show_achievements_on_profile": "Покажите свои достижения в профиле",
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
},
"achievement": {
"achievement_unlocked": "Достижение разблокировано",
"user_achievements": "Достижения {{displayName}}",
"your_achievements": "Ваши достижения",
"unlocked_at": "Разблокировано: {{date}}",
"subscription_needed": "Для просмотра этого содержимого необходима подписка на Hydra Cloud",
"new_achievements_unlocked": "Разблокировано {{achievementCount}} новых достижений из {{gameCount}} игр",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} достижений",
"achievements_unlocked_for_game": "Разблокировано {{achievementCount}} новых достижений для {{gameTitle}}",
"hidden_achievement_tooltip": "Это скрытое достижение",
"achievement_earn_points": "Заработайте {{points}} очков с этим достижением",
"earned_points": "Заработано очков:",
"available_points": "Доступные очки:",
"how_to_earn_achievements_points": "Как заработать очки достижений?"
},
"hydra_cloud": {
"subscription_tour_title": "Подписка Hydra Cloud",
"subscribe_now": "Подпишитесь прямо сейчас",
"cloud_saving": "Сохранение в облаке",
"cloud_achievements": "Сохраняйте свои достижения в облаке",
"animated_profile_picture": "Анимированные фотографии профиля",
"premium_support": "Премиальная поддержка",
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
"animated_profile_banner": "Анимированный баннер профиля",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!",
"learn_more": "Подробнее"
}
}

View file

@ -1,131 +1,423 @@
{
"language_name": "Türkçe",
"app": {
"successfully_signed_in": "Başarıyla giriş yapıldı"
},
"home": {
"featured": "Öne çıkan",
"surprise_me": "Şaşırt beni",
"no_results": "Sonuç bulunamadı"
"featured": "Öne Çıkanlar",
"surprise_me": "Beni Şaşırt",
"no_results": "Sonuç bulunamadı",
"start_typing": "Aramak için yazmaya başlayın...",
"hot": "Şu anda popüler",
"weekly": "📅 Haftanın en iyi oyunları",
"achievements": "🏆 Tamamlanacak oyunlar"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "İndirmeler",
"downloads": "İndirilenler",
"settings": "Ayarlar",
"my_library": "Kütüphane",
"downloading_metadata": "{{title}} (Metadata indiriliyor…)",
"paused": "{{title}} (Duraklatıldı)",
"my_library": "Kütüphanem",
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
"paused": "{{title}} (Durduruldu)",
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
"filter": "Kütüphaneyi filtrele",
"home": "Ana menü"
"home": "Ana Sayfa",
"queued": "{{title}} (Sırada)",
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
"sign_in": "Giriş yap",
"friends": "Arkadaşlar",
"need_help": "Yardıma mı ihtiyacınız var?"
},
"header": {
"search": "Ara",
"home": "Ana menü",
"search": "Oyunları ara",
"home": "Ana Sayfa",
"catalogue": "Katalog",
"downloads": "İndirmeler",
"downloads": "İndirilenler",
"search_results": "Arama sonuçları",
"settings": "Ayarlar"
"settings": "Ayarlar",
"version_available_install": "Sürüm {{version}} mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
"version_available_download": "Sürüm {{version}} mevcut. İndirmek için buraya tıklayın."
},
"bottom_panel": {
"no_downloads_in_progress": "İndirilen bir şey yok",
"downloading_metadata": "{{title}} metadatası indiriliyor…",
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
"no_downloads_in_progress": "Devam eden indirme yok",
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlama: {{eta}} - Hız: {{speed}}",
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)"
},
"catalogue": {
"next_page": "Sonraki sayfa",
"previous_page": "Önceki sayfa"
"search": "Filtrele…",
"developers": "Geliştiriciler",
"genres": "Türler",
"tags": "Etiketler",
"publishers": "Yayıncılar",
"download_sources": "İndirme kaynakları",
"result_count": "{{resultCount}} sonuç",
"filter_count": "{{filterCount}} mevcut",
"clear_filters": "{{filterCount}} seçili filtreyi temizle"
},
"game_details": {
"open_download_options": "İndirme seçeneklerini aç",
"download_options_zero": "İndirme seçeneği yok",
"download_options_one": "{{count}} indirme seçeneği",
"download_options_other": "{{count}} indirme seçeneği",
"updated_at": "{{updated_at}} güncellendi",
"install": "İndir",
"updated_at": "{{updated_at}} tarihinde güncellendi",
"install": "Yükle",
"resume": "Devam et",
"pause": "Duraklat",
"pause": "Durdur",
"cancel": "İptal et",
"remove": "Sil",
"space_left_on_disk": "Diskte {{space}} yer kaldı",
"eta": "Bitiş {{eta}}",
"downloading_metadata": "Metadata indiriliyor…",
"filter": "Repackleri filtrele",
"remove": "Kaldır",
"space_left_on_disk": "Diskte {{space}} boş alan kaldı",
"eta": "{{eta}} tahmini bitiş",
"calculating_eta": "Kalan süre hesaplanıyor…",
"downloading_metadata": "Meta veriler indiriliyor…",
"filter": "Paketleri filtrele",
"requirements": "Sistem gereksinimleri",
"minimum": "Minimum",
"recommended": "Önerilen",
"release_date": "{{date}} tarihinde çıktı",
"publisher": "{{publisher}} tarihinde yayınlandı",
"hours": "saatler",
"minutes": "dakikalar",
"paused": "Durduruldu",
"release_date": "{{date}} tarihinde yayımlandı",
"publisher": "{{publisher}} tarafından yayımlandı",
"hours": "saat",
"minutes": "dakika",
"amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika",
"accuracy": "%{{accuracy}} doğruluk",
"accuracy": "{{accuracy}}% doğruluk",
"add_to_library": "Kütüphaneye ekle",
"remove_from_library": "Kütüphaneden kaldır",
"no_downloads": "İndirme yok",
"play_time": "{{amount}} oynandı",
"last_time_played": "Son oynanan {{period}}",
"not_played_yet": "Bu {{title}} hiç oynanmadı",
"next_suggestion": "Sıradaki öneri",
"no_downloads": "İndirilebilir içerik yok",
"play_time": "{{amount}} süre oynandı",
"last_time_played": "Son oynama {{period}} önce",
"not_played_yet": "{{title}} henüz oynanmadı",
"next_suggestion": "Sonraki öneri",
"play": "Oyna",
"deleting": "Installer siliniyor…",
"deleting": "Yükleyici siliniyor…",
"close": "Kapat",
"playing_now": imdi oynanıyor",
"playing_now": u anda oynanıyor",
"change": "Değiştir",
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
"download_now": "Şimdi"
"repacks_modal_description": "İndirmek istediğiniz paketi seçin",
"select_folder_hint": "Varsayılan klasörü değiştirmek için <0>Ayarlar</0> bölümüne gidin",
"download_now": "Şimdi indir",
"no_shop_details": "Mağaza bilgileri alınamadı.",
"download_options": "İndirme seçenekleri",
"download_path": "İndirme yolu",
"previous_screenshot": "Önceki ekran görüntüsü",
"next_screenshot": "Sonraki ekran görüntüsü",
"screenshot": "{{number}} ekran görüntüsü",
"open_screenshot": "{{number}} ekran görüntüsünü aç",
"download_settings": "İndirme ayarları",
"downloader": "İndirici",
"select_executable": "Seç",
"no_executable_selected": "Hiçbir çalıştırılabilir dosya seçilmedi",
"open_folder": "Klasörü aç",
"open_download_location": "İndirilen dosyaları gör",
"create_shortcut": "Masaüstü kısayolu oluştur",
"clear": "Temizle",
"remove_files": "Dosyaları kaldır",
"remove_from_library_title": "Emin misiniz?",
"remove_from_library_description": "Bu işlem {{game}} oyununu kütüphanenizden kaldıracaktır",
"options": "Seçenekler",
"executable_section_title": "Çalıştırılabilir dosya",
"executable_section_description": "\"Oyna\" tıklandığında çalıştırılacak dosyanın yolu",
"downloads_secion_title": "İndirmeler",
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
"danger_zone_section_title": "Tehlike bölgesi",
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyaları kaldırın",
"download_in_progress": "İndirme devam ediyor",
"download_paused": "İndirme durduruldu",
"last_downloaded_option": "Son indirilen seçenek",
"create_shortcut_success": "Kısayol başarıyla oluşturuldu",
"create_shortcut_error": "Kısayol oluşturulurken hata oluştu",
"nsfw_content_title": "Bu oyun uygunsuz içerik içeriyor",
"nsfw_content_description": "{{title}} her yaş için uygun olmayabilecek içeriklere sahiptir. Devam etmek istediğinizden emin misiniz?",
"allow_nsfw_content": "Devam et",
"refuse_nsfw_content": "Geri dön",
"stats": "İstatistikler",
"download_count": "İndirme sayısı",
"player_count": "Aktif oyuncular",
"download_error": "Bu indirme seçeneği mevcut değil",
"download": "İndir",
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
"warning": "Uyarı:",
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
"achievements": "Başarılar",
"achievements_count": "Başarılar {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Bulut kaydı",
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
"backups": "Yedekler",
"install_backup": "Yükle",
"delete_backup": "Sil",
"create_backup": "Yeni yedek oluştur",
"last_backup_date": "{{date}} tarihindeki son yedek",
"no_backup_preview": "Bu oyun için kayıtlı oyun bulunamadı",
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
"uploading_backup": "Yedek yükleniyor…",
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
"backup_uploaded": "Yedek yüklendi",
"backup_deleted": "Yedek silindi",
"backup_restored": "Yedek geri yüklendi",
"see_all_achievements": "Tüm başarıları gör",
"sign_in_to_see_achievements": "Başarıları görmek için giriş yapın",
"mapping_method_automatic": "Otomatik",
"mapping_method_manual": "Manuel",
"mapping_method_label": "Eşleme yöntemi",
"files_automatically_mapped": "Dosyalar otomatik olarak eşlendi",
"no_backups_created": "Bu oyun için yedek oluşturulmadı",
"manage_files": "Dosyaları yönet",
"loading_save_preview": "Kayıtlı oyunlar aranıyor…",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
"launch_options": "Başlatma Seçenekleri",
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine değişiklikler girebilir (deneysel özellik)",
"launch_options_placeholder": "Belirtilen bir parametre yok",
"no_download_option_info": "Bilgi mevcut değil",
"backup_deletion_failed": "Yedek silinemedi",
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
"achievements_not_sync": "Başarılarınızı senkronize etmeyi öğrenin",
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
"select_folder": "Klasör seç",
"backup_from": "{{date}} tarihinden yedek",
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
"no_directory_selected": "Bir dizin seçilmedi",
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
"reset_achievements": "Başarıları sıfırla",
"reset_achievements_description": "Bu işlem {{game}} için tüm başarıları sıfırlar",
"reset_achievements_title": "Emin misiniz?",
"reset_achievements_success": "Başarılar başarıyla sıfırlandı",
"reset_achievements_error": "Başarılar sıfırlanamadı"
},
"activation": {
"title": "Hydra'yı aktif et",
"installation_id": "Kurulum ID'si:",
"enter_activation_code": "Aktifleştirme kodunuzu girin",
"message": "Bunu nerede soracağınızı bilmiyorsanız, buna sahip olmamanız gerekiyor.",
"activate": "Aktif et",
"title": "Hydra'yı Aktive Et",
"installation_id": "Kurulum Kimliği:",
"enter_activation_code": "Aktivasyon kodunuzu girin",
"message": "Bunu nereden soracağınızı bilmiyorsanız, bu sizin için olmamalı.",
"activate": "Aktive Et",
"loading": "Yükleniyor…"
},
"downloads": {
"resume": "Devam et",
"resume": "Devam Et",
"pause": "Duraklat",
"eta": "Bitiş {{eta}}",
"eta": "Tamamlama {{eta}}",
"paused": "Duraklatıldı",
"verifying": "Doğrulanıyor…",
"completed": "Tamamlandı",
"cancel": "İptal et",
"filter": "Yüklü oyunları filtrele",
"removed": "İndirilmedi",
"cancel": "İptal Et",
"filter": "İndirilen oyunları filtrele",
"remove": "Kaldır",
"downloading_metadata": "Metadata indiriliyor…",
"deleting": "Installer siliniyor…",
"delete": "Installer'ı sil",
"deleting": "Yükleyici siliniyor…",
"delete": "Yükleyiciyi kaldır",
"delete_modal_title": "Emin misiniz?",
"delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek",
"install": "Kur"
"delete_modal_description": "Bu işlem, tüm kurulum dosyalarını bilgisayarınızdan kaldıracaktır",
"install": "Kur",
"download_in_progress": "Devam ediyor",
"queued_downloads": "Sıradaki indirmeler",
"downloads_completed": "Tamamlananlar",
"queued": "Sırada",
"no_downloads_title": "Bomboş",
"no_downloads_description": "Henüz Hydra ile hiçbir şey indirmediniz, ancak başlamak için asla geç değil.",
"checking_files": "Dosyalar kontrol ediliyor…",
"seeding": "Paylaşılıyor",
"stop_seeding": "Paylaşımı durdur",
"resume_seeding": "Paylaşımı sürdür",
"options": "Yönet"
},
"settings": {
"downloads_path": "İndirme yolu",
"change": "Güncelle",
"notifications": "Bildirimler",
"enable_download_notifications": "Bir indirme bittiğinde",
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde"
"enable_download_notifications": "Bir indirme tamamlandığında",
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
"quit_app_instead_hiding": "Hydra'yı kapatırken gizlemeyin",
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
"general": "Genel",
"behavior": "Davranış",
"download_sources": "İndirme kaynakları",
"language": "Dil",
"real_debrid_api_token": "API Anahtarı",
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
"real_debrid_invalid_token": "Geçersiz API anahtarı",
"real_debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
"real_debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
"save_changes": "Değişiklikleri Kaydet",
"changes_saved": "Değişiklikler başarıyla kaydedildi",
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
"validate_download_source": "Doğrula",
"remove_download_source": "Kaldır",
"add_download_source": "Kaynak ekle",
"download_count_zero": "İndirme seçeneği yok",
"download_count_one": "{{countFormatted}} indirme seçeneği",
"download_count_other": "{{countFormatted}} indirme seçeneği",
"download_source_url": "İndirme kaynağı URL'si",
"add_download_source_description": ".json dosyasının URL'sini girin",
"download_source_up_to_date": "Güncel",
"download_source_errored": "Hatalı",
"sync_download_sources": "Kaynakları senkronize et",
"removed_download_source": "İndirme kaynağı kaldırıldı",
"added_download_source": "İndirme kaynağı eklendi",
"download_sources_synced": "Tüm indirme kaynakları senkronize edildi",
"insert_valid_json_url": "Geçerli bir JSON URL'si girin",
"found_download_option_zero": "Hiçbir indirme seçeneği bulunamadı",
"found_download_option_one": "{{countFormatted}} indirme seçeneği bulundu",
"found_download_option_other": "{{countFormatted}} indirme seçeneği bulundu",
"import": "İçe aktar",
"public": "Herkese açık",
"private": "Gizli",
"friends_only": "Sadece arkadaşlar",
"privacy": "Gizlilik",
"profile_visibility": "Profil görünürlüğü",
"profile_visibility_description": "Profilinizi ve kütüphanenizi kimlerin görebileceğini seçin",
"required_field": "Bu alan gereklidir",
"source_already_exists": "Bu kaynak zaten eklenmiş",
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
"blocked_users": "Engellenen kullanıcılar",
"user_unblocked": "Kullanıcının engeli kaldırıldı",
"enable_achievement_notifications": "Bir başarı kilidi açıldığında",
"launch_minimized": "Hydra'yı küçültülmüş başlat",
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
"show_hidden_achievement_description": "Gizli başarııklamalarını kilitlenmeden önce göster"
},
"notifications": {
"download_complete": "İndirme tamamlandı",
"game_ready_to_install": "{{title}} kuruluma hazır",
"game_ready_to_install": "{{title}} kurulmaya hazır",
"repack_list_updated": "Repack listesi güncellendi",
"repack_count_one": "{{count}} yeni repack eklendi",
"repack_count_other": "{{count}} yeni repack eklendi"
"repack_count_one": "{{count}} repack eklendi",
"repack_count_other": "{{count}} repack eklendi",
"new_update_available": "Sürüm {{version}} mevcut",
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
"notification_achievement_unlocked_title": "{{game}} için başarı kilidi açıldı",
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarılar açıldı"
},
"system_tray": {
"open": "Hydra'yı aç",
"open": "Hydra'yı Aç",
"quit": ık"
},
"game_card": {
"no_downloads": "İndirme mevcut değil"
"no_downloads": "İndirilebilir içerik bulunmuyor"
},
"binary_not_found_modal": {
"title": "Programlar yüklü değil",
"description": "Sisteminizde Wine veya Lutris çalıştırılabiliri bulunamadı",
"instructions": "Oyunları düzgün şekilde çalıştırmak için Linux distronuza bunlardan birini nasıl yükleyebileceğinize bakın"
"title": "Programlar Yüklü Değil",
"description": "Wine veya Lutris çalıştırılabilir dosyaları sisteminizde bulunamadı",
"instructions": "Oyunun normal çalışabilmesi için bunlardan herhangi birini Linux dağıtımınıza uygun şekilde nasıl kuracağınızı kontrol edin"
},
"modal": {
"close": "Kapat tuşu"
"close": "Kapat düğmesi"
},
"forms": {
"toggle_password_visibility": "Şifre görünürlüğünü değiştir"
},
"user_profile": {
"amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika",
"last_time_played": "Son oynanma {{period}}",
"activity": "Son Etkinlik",
"library": "Kütüphane",
"total_play_time": "Toplam oynama süresi",
"no_recent_activity_title": "Hmmm… burada bir şey yok",
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamamışsınız. Bunu değiştirmenin zamanı geldi!",
"display_name": "Görünen isim",
"saving": "Kaydediliyor",
"save": "Kaydet",
"edit_profile": "Profili Düzenle",
"saved_successfully": "Başarıyla kaydedildi",
"try_again": "Lütfen tekrar deneyin",
"sign_out_modal_title": "Emin misiniz?",
"cancel": "İptal",
"successfully_signed_out": "Başarıyla çıkış yapıldı",
"sign_out": ıkış yap",
"playing_for": "{{amount}} oynanıyor",
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaptığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Çıkışa devam etmek istiyor musunuz?",
"add_friends": "Arkadaş Ekle",
"add": "Ekle",
"friend_code": "Arkadaş kodu",
"see_profile": "Profili gör",
"sending": "Gönderiliyor",
"friend_request_sent": "Arkadaşlık isteği gönderildi",
"friends": "Arkadaşlar",
"friends_list": "Arkadaş listesi",
"user_not_found": "Kullanıcı bulunamadı",
"block_user": "Kullanıcıyı engelle",
"add_friend": "Arkadaş ekle",
"request_sent": "İstek gönderildi",
"request_received": "İstek alındı",
"accept_request": "İsteği kabul et",
"ignore_request": "İsteği yok say",
"cancel_request": "İsteği iptal et",
"undo_friendship": "Arkadaşlığı sonlandır",
"request_accepted": "İstek kabul edildi",
"user_blocked_successfully": "Kullanıcı başarıyla engellendi",
"user_block_modal_text": "Bu işlem {{displayName}} adlı kullanıcıyı engelleyecek",
"blocked_users": "Engellenen kullanıcılar",
"unblock": "Engeli kaldır",
"no_friends_added": "Hiç arkadaş eklemediniz",
"pending": "Bekliyor",
"no_pending_invites": "Bekleyen davetiniz yok",
"no_blocked_users": "Engellenmiş kullanıcı yok",
"friend_code_copied": "Arkadaş kodu kopyalandı",
"undo_friendship_modal_text": "Bu işlem {{displayName}} ile arkadaşlığınızı sonlandıracak",
"privacy_hint": "Bunu kimin görebileceğini ayarlamak için <0>Ayarlar</0> bölümüne gidin",
"locked_profile": "Bu profil gizli",
"image_process_failure": "Görüntü işleme başarısız oldu",
"required_field": "Bu alan gerekli",
"displayname_min_length": "Görünen isim en az 3 karakter uzunluğunda olmalıdır",
"displayname_max_length": "Görünen isim en fazla 50 karakter uzunluğunda olabilir",
"report_profile": "Bu profili bildir",
"report_reason": "Bu profili neden bildiriyorsunuz?",
"report_description": "Ek bilgi",
"report_description_placeholder": "Ek bilgi",
"report": "Bildir",
"report_reason_hate": "Nefret söylemi",
"report_reason_sexual_content": "Cinsel içerik",
"report_reason_violence": "Şiddet",
"report_reason_spam": "Spam",
"report_reason_other": "Diğer",
"profile_reported": "Profil bildirildi",
"your_friend_code": "Arkadaş kodunuz:",
"upload_banner": "Afiş yükle",
"uploading_banner": "Afiş yükleniyor…",
"background_image_updated": "Arka plan görüntüsü güncellendi",
"stats": "İstatistikler",
"achievements": "Başarılar",
"games": "Oyunlar",
"top_percentile": "En üst {{percentile}}%",
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
"playing": "{{game}} oynanıyor",
"achievements_unlocked": "Başarılar açıldı",
"earned_points": "Kazanılan puanlar",
"show_achievements_on_profile": "Başarılarınızı profilinizde gösterin",
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
},
"achievement": {
"achievement_unlocked": "Başarııldı",
"user_achievements": "{{displayName}}'in Başarıları",
"your_achievements": "Başarılarınız",
"unlocked_at": "Açılma zamanı: {{date}}",
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarııldı",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarııldı",
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
"achievement_earn_points": "Bu başarı ile {{points}} puan kazanın",
"earned_points": "Kazanılan puanlar:",
"available_points": "Mevcut puanlar:",
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Aboneliği",
"subscribe_now": "Şimdi abone olun",
"cloud_saving": "Bulut kaydetme",
"cloud_achievements": "Başarılarınızı buluta kaydedin",
"animated_profile_picture": "Animasyonlu profil resimleri",
"premium_support": "Premium Destek",
"show_and_compare_achievements": "Başarılarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
"animated_profile_banner": "Animasyonlu profil afişi",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
"learn_more": "Daha Fazla Bilgi Edinin"
}
}

View file

@ -231,7 +231,7 @@
"sign_out_modal_text": "Ваша бібліотека пов'язана з поточним обліковим записом. При виході з системи ваша бібліотека буде недоступною, і прогрес не буде збережено. Продовжити вихід?",
"sign_out_modal_title": "Ви впевнені?",
"successfully_signed_out": "Успішний вихід з акаунту",
"total_play_time": "Всього зіграно: {{amount}}",
"total_play_time": "Всього зіграно",
"try_again": "Будь ласка, попробуйте ще раз"
}
}

View file

@ -290,7 +290,7 @@
"last_time_played": "上次游玩时间 {{period}}",
"activity": "近期活动",
"library": "库",
"total_play_time": "总游戏时长: {{amount}}",
"total_play_time": "总游戏时长",
"no_recent_activity_title": "Emmm… 这里暂时啥都没有",
"no_recent_activity_description": "你最近没玩过任何游戏。是时候做出改变了!",
"display_name": "昵称",
@ -359,11 +359,11 @@
"achievement_unlocked": "成就已解锁",
"user_achievements": "{{displayName}}的成就",
"your_achievements": "你的成就",
"unlocked_at": "解锁于:",
"unlocked_at": "解锁于: {{date}}",
"subscription_needed": "需要订阅 Hydra Cloud 才能看到此内容",
"new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就"
},
"tour": {
"hydra_cloud": {
"subscription_tour_title": "Hydra 云订阅",
"subscribe_now": "现在订购",
"cloud_saving": "云存档",

View file

@ -1,10 +1,8 @@
import { DataSource } from "typeorm";
import {
DownloadQueue,
DownloadSource,
Game,
GameShopCache,
Repack,
UserPreferences,
UserAuth,
GameAchievement,
@ -17,12 +15,10 @@ export const dataSource = new DataSource({
type: "better-sqlite3",
entities: [
Game,
Repack,
UserAuth,
UserPreferences,
UserSubscription,
GameShopCache,
DownloadSource,
DownloadQueue,
GameAchievement,
],

View file

@ -1,41 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from "typeorm";
import type { Repack } from "./repack.entity";
import { DownloadSourceStatus } from "@shared";
@Entity("download_source")
export class DownloadSource {
@PrimaryGeneratedColumn()
id: number;
@Column("text", { nullable: true, unique: true })
url: string;
@Column("text")
name: string;
@Column("text", { nullable: true })
etag: string | null;
@Column("int", { default: 0 })
downloadCount: number;
@Column("text", { default: DownloadSourceStatus.UpToDate })
status: DownloadSourceStatus;
@OneToMany("Repack", "downloadSource", { cascade: true })
repacks: Repack[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View file

@ -5,9 +5,7 @@ import {
CreateDateColumn,
UpdateDateColumn,
OneToOne,
JoinColumn,
} from "typeorm";
import { Repack } from "./repack.entity";
import type { GameShop, GameStatus } from "@types";
import { Downloader } from "@shared";
@ -39,6 +37,9 @@ export class Game {
@Column("text", { nullable: true })
executablePath: string | null;
@Column("text", { nullable: true })
launchOptions: string | null;
@Column("text", { nullable: true })
winePrefixPath: string | null;
@ -72,19 +73,15 @@ export class Game {
@Column("text", { nullable: true })
uri: string | null;
/**
* @deprecated
*/
@OneToOne("Repack", "game", { nullable: true })
@JoinColumn()
repack: Repack;
@OneToOne("DownloadQueue", "game")
downloadQueue: DownloadQueue;
@Column("boolean", { default: false })
isDeleted: boolean;
@Column("boolean", { default: false })
shouldSeed: boolean;
@CreateDateColumn()
createdAt: Date;

View file

@ -1,10 +1,8 @@
export * from "./game.entity";
export * from "./repack.entity";
export * from "./user-auth.entity";
export * from "./user-preferences.entity";
export * from "./user-subscription.entity";
export * from "./game-shop-cache.entity";
export * from "./game.entity";
export * from "./game-achievements.entity";
export * from "./download-source.entity";
export * from "./download-queue.entity";

View file

@ -1,45 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
} from "typeorm";
import { DownloadSource } from "./download-source.entity";
@Entity("repack")
export class Repack {
@PrimaryGeneratedColumn()
id: number;
@Column("text", { unique: true })
title: string;
/**
* @deprecated Use uris instead
*/
@Column("text", { unique: true })
magnet: string;
@Column("text")
repacker: string;
@Column("text")
fileSize: string;
@Column("datetime")
uploadDate: Date | string;
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
downloadSource: DownloadSource;
@Column("text", { default: "[]" })
uris: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View file

@ -41,6 +41,12 @@ export class UserPreferences {
@Column("boolean", { default: false })
disableNsfwAlert: boolean;
@Column("boolean", { default: true })
seedAfterDownloadComplete: boolean;
@Column("boolean", { default: false })
showHiddenAchievementsDescription: boolean;
@CreateDateColumn()
createdAt: Date;

View file

@ -9,6 +9,8 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
if (!auth) return null;
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
if (!payload) return null;
return payload.sessionId;
};

View file

@ -1,12 +1,8 @@
import { registerEvent } from "../register-event";
import {
DownloadManager,
HydraApi,
PythonInstance,
gamesPlaytime,
} from "@main/services";
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
import { dataSource } from "@main/data-source";
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
import { PythonRPC } from "@main/services/python-rpc";
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
const databaseOperations = dataSource
@ -32,7 +28,7 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
DownloadManager.cancelDownload();
/* Disconnects libtorrent */
PythonInstance.killTorrent();
PythonRPC.kill();
HydraApi.handleSignOut();

View file

@ -1,9 +1,6 @@
import type { GameShop } from "@types";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { CatalogueCategory, steamUrlBuilder } from "@shared";
import { steamGamesWorker } from "@main/workers";
import { CatalogueCategory } from "@shared";
const getCatalogue = async (
_event: Electron.IpcMainInvokeEvent,
@ -14,26 +11,11 @@ const getCatalogue = async (
skip: "0",
});
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
return HydraApi.get(
`/catalogue/${category}?${params.toString()}`,
{},
{ needsAuth: false }
);
return Promise.all(
response.map(async (game) => {
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
name: "getById",
});
return {
title: steamGame.name,
shop: game.shop,
cover: steamUrlBuilder.library(game.objectId),
objectId: game.objectId,
};
})
);
};
registerEvent("getCatalogue", getCatalogue);

View file

@ -0,0 +1,10 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
const getDevelopers = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<string[]>(`/catalogue/developers`, null, {
needsAuth: false,
});
};
registerEvent("getDevelopers", getDevelopers);

View file

@ -1,29 +0,0 @@
import type { CatalogueEntry } from "@types";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { steamUrlBuilder } from "@shared";
const getGames = async (
_event: Electron.IpcMainInvokeEvent,
take = 12,
skip = 0
): Promise<CatalogueEntry[]> => {
const searchParams = new URLSearchParams({
take: take.toString(),
skip: skip.toString(),
});
const games = await HydraApi.get<CatalogueEntry[]>(
`/games/catalogue?${searchParams.toString()}`,
undefined,
{ needsAuth: false }
);
return games.map((game) => ({
...game,
cover: steamUrlBuilder.library(game.objectId),
}));
};
registerEvent("getGames", getGames);

View file

@ -0,0 +1,10 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
const getPublishers = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<string[]>(`/catalogue/publishers`, null, {
needsAuth: false,
});
};
registerEvent("getPublishers", getPublishers);

View file

@ -1,23 +1,18 @@
import type { CatalogueSearchPayload } from "@types";
import { registerEvent } from "../register-event";
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
import type { CatalogueEntry } from "@types";
import { HydraApi } from "@main/services";
const searchGamesEvent = async (
const searchGames = async (
_event: Electron.IpcMainInvokeEvent,
query: string
): Promise<CatalogueEntry[]> => {
const games = await HydraApi.get<
{ objectId: string; title: string; shop: string }[]
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
return games.map((game) => {
return convertSteamGameToCatalogueEntry({
id: Number(game.objectId),
name: game.title,
clientIcon: null,
});
});
payload: CatalogueSearchPayload,
take: number,
skip: number
) => {
return HydraApi.post(
"/catalogue/search",
{ ...payload, take, skip },
{ needsAuth: false }
);
};
registerEvent("searchGames", searchGamesEvent);
registerEvent("searchGames", searchGames);

View file

@ -1,6 +1,7 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
import type { GameArtifact, GameShop } from "@types";
import { SubscriptionRequiredError, UserNotLoggedInError } from "@shared";
const getGameArtifacts = async (
_event: Electron.IpcMainInvokeEvent,
@ -13,8 +14,20 @@ const getGameArtifacts = async (
});
return HydraApi.get<GameArtifact[]>(
`/profile/games/artifacts?${params.toString()}`
);
`/profile/games/artifacts?${params.toString()}`,
{},
{ needsSubscription: true }
).catch((err) => {
if (err instanceof SubscriptionRequiredError) {
return [];
}
if (err instanceof UserNotLoggedInError) {
return [];
}
throw err;
});
};
registerEvent("getGameArtifacts", getGameArtifacts);

View file

@ -89,7 +89,7 @@ const uploadSaveGame = async (
"Content-Type": "application/tar",
},
onUploadProgress: (progressEvent) => {
console.log(progressEvent);
logger.log(progressEvent);
},
});

View file

@ -1,9 +0,0 @@
import { registerEvent } from "../register-event";
import { knexClient } from "@main/knex-client";
const deleteDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
id: number
) => knexClient("download_source").where({ id }).delete();
registerEvent("deleteDownloadSource", deleteDownloadSource);

View file

@ -1,7 +0,0 @@
import { registerEvent } from "../register-event";
import { knexClient } from "@main/knex-client";
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
knexClient.select("*").from("download_source");
registerEvent("getDownloadSources", getDownloadSources);

View file

@ -0,0 +1,17 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
const putDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
objectIds: string[]
) => {
return HydraApi.put<{ fingerprint: string }>(
"/download-sources",
{
objectIds,
},
{ needsAuth: false }
);
};
registerEvent("putDownloadSource", putDownloadSource);

View file

@ -0,0 +1,15 @@
import fs from "node:fs";
import { registerEvent } from "../register-event";
const checkFolderWritePermission = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) =>
new Promise((resolve) => {
fs.access(path, fs.constants.W_OK, (err) => {
resolve(!err);
});
});
registerEvent("checkFolderWritePermission", checkFolderWritePermission);

View file

@ -1,10 +1,10 @@
import checkDiskSpace from "check-disk-space";
import disk from "diskusage";
import { registerEvent } from "../register-event";
const getDiskFreeSpace = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) => checkDiskSpace(path);
) => disk.check(path);
registerEvent("getDiskFreeSpace", getDiskFreeSpace);

View file

@ -1,31 +0,0 @@
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
import { steamGamesWorker } from "@main/workers";
import { steamUrlBuilder } from "@shared";
export interface SearchGamesArgs {
query?: string;
take?: number;
skip?: number;
}
export const convertSteamGameToCatalogueEntry = (
game: SteamGame
): CatalogueEntry => ({
objectId: String(game.id),
title: game.name,
shop: "steam" as GameShop,
cover: steamUrlBuilder.library(String(game.id)),
});
export const getSteamGameById = async (
objectId: string
): Promise<CatalogueEntry | null> => {
const steamGame = await steamGamesWorker.run(Number(objectId), {
name: "getById",
});
if (!steamGame) return null;
return convertSteamGameToCatalogueEntry(steamGame);
};

View file

@ -3,13 +3,15 @@ import { ipcMain } from "electron";
import "./catalogue/get-catalogue";
import "./catalogue/get-game-shop-details";
import "./catalogue/get-games";
import "./catalogue/get-how-long-to-beat";
import "./catalogue/get-random-game";
import "./catalogue/search-games";
import "./catalogue/get-game-stats";
import "./catalogue/get-trending-games";
import "./catalogue/get-publishers";
import "./catalogue/get-developers";
import "./hardware/get-disk-free-space";
import "./hardware/check-folder-write-permission";
import "./library/add-game-to-library";
import "./library/create-game-shortcut";
import "./library/close-game";
@ -21,25 +23,30 @@ import "./library/open-game-executable-path";
import "./library/open-game-installer";
import "./library/open-game-installer-path";
import "./library/update-executable-path";
import "./library/update-launch-options";
import "./library/verify-executable-path";
import "./library/remove-game";
import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";
import "./misc/get-features";
import "./misc/show-item-in-folder";
import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
import "./torrenting/start-game-download";
import "./torrenting/pause-game-seed";
import "./torrenting/resume-game-seed";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid";
import "./download-sources/delete-download-source";
import "./download-sources/get-download-sources";
import "./download-sources/put-download-source";
import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
@ -68,7 +75,6 @@ import "./cloud-save/delete-game-artifact";
import "./cloud-save/select-game-backup-path";
import "./notifications/publish-new-repacks-notification";
import { isPortableVersion } from "@main/helpers";
import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion);

View file

@ -1,8 +1,10 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { PythonInstance, logger } from "@main/services";
import { logger } from "@main/services";
import sudo from "sudo-prompt";
import { app } from "electron";
import { PythonRPC } from "@main/services/python-rpc";
import { ProcessPayload } from "@main/services/download/types";
const getKillCommand = (pid: number) => {
if (process.platform == "win32") {
@ -16,7 +18,10 @@ const closeGame = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
const processes = await PythonInstance.getProcessList();
const processes =
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
[];
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});

View file

@ -7,11 +7,16 @@ import { parseExecutablePath } from "../helpers/parse-executable-path";
const openGame = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number,
executablePath: string
executablePath: string,
launchOptions: string | null
) => {
// TODO: revisit this for launchOptions
const parsedPath = parseExecutablePath(executablePath);
await gameRepository.update({ id: gameId }, { executablePath: parsedPath });
await gameRepository.update(
{ id: gameId },
{ executablePath: parsedPath, launchOptions }
);
shell.openPath(parsedPath);
};

View file

@ -0,0 +1,56 @@
import { gameAchievementRepository, gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
import fs from "fs";
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
const resetGameAchievements = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
try {
const game = await gameRepository.findOne({ where: { id: gameId } });
if (!game) return;
const achievementFiles = findAchievementFiles(game);
if (achievementFiles.length) {
for (const achievementFile of achievementFiles) {
achievementsLogger.log(`deleting ${achievementFile.filePath}`);
await fs.promises.rm(achievementFile.filePath);
}
}
await gameAchievementRepository.update(
{ objectId: game.objectID },
{
unlockedAchievements: null,
}
);
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
() =>
achievementsLogger.log(
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
)
);
const gameAchievements = await getUnlockedAchievements(
game.objectID,
game.shop,
true
);
WindowManager.mainWindow?.webContents.send(
`on-update-achievements-${game.objectID}-${game.shop}`,
gameAchievements
);
} catch (error) {
achievementsLogger.error(error);
throw error;
}
};
registerEvent("resetGameAchievements", resetGameAchievements);

View file

@ -0,0 +1,19 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
const updateLaunchOptions = async (
_event: Electron.IpcMainInvokeEvent,
id: number,
launchOptions: string | null
) => {
return gameRepository.update(
{
id,
},
{
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
}
);
};
registerEvent("updateLaunchOptions", updateLaunchOptions);

View file

@ -0,0 +1,8 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
};
registerEvent("getFeatures", getFeatures);

View file

@ -1,16 +1,10 @@
import { shell } from "electron";
import { registerEvent } from "../register-event";
import {
userAuthRepository,
userPreferencesRepository,
} from "@main/repository";
import { userAuthRepository } from "@main/repository";
import { HydraApi } from "@main/services";
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const [userAuth, userPreferences] = await Promise.all([
userAuthRepository.findOne({ where: { id: 1 } }),
userPreferencesRepository.findOne({ where: { id: 1 } }),
]);
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
if (!userAuth) {
return;
@ -22,7 +16,6 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const params = new URLSearchParams({
token: paymentToken,
lng: userPreferences?.language || "en",
});
shell.openExternal(

View file

@ -1,11 +1,16 @@
import { registerEvent } from "../register-event";
import { PythonInstance } from "@main/services";
import { PythonRPC } from "@main/services/python-rpc";
const processProfileImage = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) => {
return PythonInstance.processProfileImage(path);
return PythonRPC.rpc
.post<{
imagePath: string;
mimeType: string;
}>("/profile-image", { image_path: path })
.then((response) => response.data);
};
registerEvent("processProfileImage", processProfileImage);

View file

@ -0,0 +1,17 @@
import { registerEvent } from "../register-event";
import { DownloadManager } from "@main/services";
import { gameRepository } from "@main/repository";
const pauseGameSeed = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
await gameRepository.update(gameId, {
status: "complete",
shouldSeed: false,
});
await DownloadManager.pauseSeeding(gameId);
};
registerEvent("pauseGameSeed", pauseGameSeed);

View file

@ -0,0 +1,29 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
import { DownloadManager } from "@main/services";
import { Downloader } from "@shared";
const resumeGameSeed = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
const game = await gameRepository.findOne({
where: {
id: gameId,
isDeleted: false,
downloader: Downloader.Torrent,
progress: 1,
},
});
if (!game) return;
await gameRepository.update(gameId, {
status: "seeding",
shouldSeed: true,
});
await DownloadManager.resumeSeeding(game);
};
registerEvent("resumeGameSeed", resumeGameSeed);

View file

@ -1,6 +1,6 @@
import { registerEvent } from "../register-event";
import type { StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services";
import { DownloadManager, HydraApi } from "@main/services";
import { Not } from "typeorm";
import { steamGamesWorker } from "@main/workers";
@ -76,24 +76,23 @@ const startGameDownload = async (
},
});
createGame(updatedGame!).catch(() => {});
HydraApi.post(
"/games/download",
{
objectId: updatedGame!.objectID,
shop: updatedGame!.shop,
},
{ needsAuth: false }
).catch((err) => {
logger.error("Failed to create game download", err);
});
await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!);
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
await Promise.all([
createGame(updatedGame!).catch(() => {}),
HydraApi.post(
"/games/download",
{
objectId: updatedGame!.objectID,
shop: updatedGame!.shop,
},
{ needsAuth: false }
).catch(() => {}),
]);
});
};

View file

@ -1,4 +1,4 @@
import { RealDebridClient } from "@main/services/real-debrid";
import { RealDebridClient } from "@main/services/download/real-debrid";
import { registerEvent } from "../register-event";
const authenticateRealDebrid = async (

View file

@ -13,6 +13,9 @@ const getComparedUnlockedAchievements = async (
where: { id: 1 },
});
const showHiddenAchievementsDescription =
userPreferences?.showHiddenAchievementsDescription || false;
return HydraApi.get<ComparedAchievements>(
`/users/${userId}/games/achievements/compare`,
{
@ -21,15 +24,35 @@ const getComparedUnlockedAchievements = async (
language: userPreferences?.language || "en",
}
).then((achievements) => {
const sortedAchievements = achievements.achievements.sort((a, b) => {
if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1;
if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1;
if (a.targetStat.unlocked && b.targetStat.unlocked) {
return b.targetStat.unlockTime! - a.targetStat.unlockTime!;
}
const sortedAchievements = achievements.achievements
.sort((a, b) => {
if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1;
if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1;
if (a.targetStat.unlocked && b.targetStat.unlocked) {
return b.targetStat.unlockTime! - a.targetStat.unlockTime!;
}
return Number(a.hidden) - Number(b.hidden);
});
return Number(a.hidden) - Number(b.hidden);
})
.map((achievement) => {
if (!achievement.hidden) return achievement;
if (!achievement.ownerStat) {
return {
...achievement,
description: "",
};
}
if (!showHiddenAchievementsDescription && achievement.hidden) {
return {
...achievement,
description: "",
};
}
return achievement;
});
return {
...achievements,

View file

@ -1,6 +1,9 @@
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types";
import { registerEvent } from "../register-event";
import { gameAchievementRepository } from "@main/repository";
import {
gameAchievementRepository,
userPreferencesRepository,
} from "@main/repository";
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
export const getUnlockedAchievements = async (
@ -12,10 +15,17 @@ export const getUnlockedAchievements = async (
where: { objectId, shop },
});
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
const showHiddenAchievementsDescription =
userPreferences?.showHiddenAchievementsDescription || false;
const achievementsData = await getGameAchievementData(
objectId,
shop,
useCachedData
useCachedData ? cachedAchievements : null
);
const unlockedAchievements = JSON.parse(
@ -50,6 +60,10 @@ export const getUnlockedAchievements = async (
unlocked: false,
unlockTime: null,
icongray: icongray,
description:
!achievementData.hidden || showHiddenAchievementsDescription
? achievementData.description
: undefined,
} as UserAchievement;
})
.sort((a, b) => {

View file

@ -11,7 +11,7 @@ const getSteamGame = async (objectId: string) => {
});
return {
title: steamGame.name,
title: steamGame.name as string,
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
};
} catch (err) {
@ -67,8 +67,25 @@ const getUser = async (
}
}
const friends = await Promise.all(
profile.friends.map(async (friend) => {
if (!friend.currentGame) return friend;
const currentGame = await getSteamGame(friend.currentGame.objectId);
return {
...friend,
currentGame: {
...friend.currentGame,
...currentGame,
},
};
})
);
return {
...profile,
friends,
libraryGames,
recentGames,
};

View file

@ -5,12 +5,14 @@ import path from "node:path";
import url from "node:url";
import fs from "node:fs";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, PythonInstance, WindowManager } from "@main/services";
import { logger, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source";
import resources from "@locales";
import { userPreferencesRepository } from "@main/repository";
import { knexClient, migrationConfig } from "./knex-client";
import { databaseDirectory } from "./constants";
import { PythonRPC } from "./services/python-rpc";
import { Aria2 } from "./services/aria2";
const { autoUpdater } = updater;
@ -146,7 +148,8 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
/* Disconnects libtorrent */
PythonInstance.kill();
PythonRPC.kill();
Aria2.kill();
});
app.on("activate", () => {

View file

@ -13,6 +13,11 @@ import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_backgroun
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disable_nsfw_alert_column";
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game";
export type HydraMigration = Knex.Migration & { name: string };
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
@ -30,6 +35,10 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
AddWinePrefixToGame,
AddStartMinimizedColumn,
AddDisableNsfwAlertColumn,
AddShouldSeedColumn,
AddSeedAfterDownloadColumn,
AddHiddenAchievementDescriptionColumn,
AddLaunchOptionsColumnToGame,
]);
}
getMigrationName(migration: HydraMigration): string {

View file

@ -1,21 +1,22 @@
import {
DownloadManager,
Ludusavi,
PythonInstance,
startMainLoop,
} from "./services";
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
import {
downloadQueueRepository,
gameRepository,
userPreferencesRepository,
} from "./repository";
import { UserPreferences } from "./entity";
import { RealDebridClient } from "./services/real-debrid";
import { RealDebridClient } from "./services/download/real-debrid";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
import { Aria2 } from "./services/aria2";
import { Downloader } from "@shared";
import { IsNull, Not } from "typeorm";
const loadState = async (userPreferences: UserPreferences | null) => {
import("./events");
Aria2.spawn();
if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
}
@ -35,11 +36,16 @@ const loadState = async (userPreferences: UserPreferences | null) => {
},
});
if (nextQueueItem?.game.status === "active") {
DownloadManager.startDownload(nextQueueItem.game);
} else {
PythonInstance.spawn();
}
const seedList = await gameRepository.find({
where: {
shouldSeed: true,
downloader: Downloader.Torrent,
progress: 1,
uri: Not(IsNull()),
},
});
await DownloadManager.startRPC(nextQueueItem?.game, seedList);
startMainLoop();
};

View file

@ -0,0 +1,17 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddShouldSeedColumn: HydraMigration = {
name: "AddShouldSeedColumn",
up: (knex: Knex) => {
return knex.schema.alterTable("game", (table) => {
return table.boolean("shouldSeed").notNullable().defaultTo(true);
});
},
down: async (knex: Knex) => {
return knex.schema.alterTable("game", (table) => {
return table.dropColumn("shouldSeed");
});
},
};

View file

@ -0,0 +1,20 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddSeedAfterDownloadColumn: HydraMigration = {
name: "AddSeedAfterDownloadColumn",
up: (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table
.boolean("seedAfterDownloadComplete")
.notNullable()
.defaultTo(true);
});
},
down: async (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.dropColumn("seedAfterDownloadComplete");
});
},
};

View file

@ -0,0 +1,20 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddHiddenAchievementDescriptionColumn: HydraMigration = {
name: "AddHiddenAchievementDescriptionColumn",
up: (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table
.boolean("showHiddenAchievementsDescription")
.notNullable()
.defaultTo(0);
});
},
down: async (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.dropColumn("showHiddenAchievementsDescription");
});
},
};

View file

@ -0,0 +1,17 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddLaunchOptionsColumnToGame: HydraMigration = {
name: "AddLaunchOptionsColumnToGame",
up: (knex: Knex) => {
return knex.schema.alterTable("game", (table) => {
return table.string("launchOptions").nullable();
});
},
down: async (knex: Knex) => {
return knex.schema.alterTable("game", (table) => {
return table.dropColumn("launchOptions");
});
},
};

View file

@ -1,10 +1,8 @@
import { dataSource } from "./data-source";
import {
DownloadQueue,
DownloadSource,
Game,
GameShopCache,
Repack,
UserPreferences,
UserAuth,
GameAchievement,
@ -13,16 +11,11 @@ import {
export const gameRepository = dataSource.getRepository(Game);
export const repackRepository = dataSource.getRepository(Repack);
export const userPreferencesRepository =
dataSource.getRepository(UserPreferences);
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
export const downloadSourceRepository =
dataSource.getRepository(DownloadSource);
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
export const userAuthRepository = dataSource.getRepository(UserAuth);

View file

@ -1,40 +0,0 @@
import path from "node:path";
import fs from "node:fs";
import { getSteamGameClientIcon, logger } from "@main/services";
import { chunk } from "lodash-es";
import { seedsPath } from "@main/constants";
import type { SteamGame } from "@types";
const steamGamesPath = path.join(seedsPath, "steam-games.json");
const steamGames = JSON.parse(
fs.readFileSync(steamGamesPath, "utf-8")
) as SteamGame[];
const chunks = chunk(steamGames, 1500);
for (const chunk of chunks) {
await Promise.all(
chunk.map(async (steamGame) => {
if (steamGame.clientIcon) return;
const index = steamGames.findIndex((game) => game.id === steamGame.id);
try {
const clientIcon = await getSteamGameClientIcon(String(steamGame.id));
steamGames[index].clientIcon = clientIcon;
logger.log("info", `Set ${steamGame.name} client icon`);
} catch (err) {
steamGames[index].clientIcon = null;
logger.log("info", `Could not set icon for ${steamGame.name}`);
}
})
);
fs.writeFileSync(steamGamesPath, JSON.stringify(steamGames));
logger.log("info", "Updated steam games");
}

View file

@ -236,24 +236,28 @@ export class AchievementWatcherManager {
};
public static preSearchAchievements = async () => {
const newAchievementsCount =
process.platform === "win32"
? await this.preSearchAchievementsWindows()
: await this.preSearchAchievementsWithWine();
try {
const newAchievementsCount =
process.platform === "win32"
? await this.preSearchAchievementsWindows()
: await this.preSearchAchievementsWithWine();
const totalNewGamesWithAchievements = newAchievementsCount.filter(
(achievements) => achievements
).length;
const totalNewAchievements = newAchievementsCount.reduce(
(acc, val) => acc + val,
0
);
if (totalNewAchievements > 0) {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
const totalNewGamesWithAchievements = newAchievementsCount.filter(
(achievements) => achievements
).length;
const totalNewAchievements = newAchievementsCount.reduce(
(acc, val) => acc + val,
0
);
if (totalNewAchievements > 0) {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
);
}
} catch (err) {
achievementsLogger.error("Error on preSearchAchievements", err);
}
this.hasFinishedMergingWithRemote = true;

View file

@ -6,20 +6,15 @@ import { HydraApi } from "../hydra-api";
import type { AchievementData, GameShop } from "@types";
import { UserNotLoggedInError } from "@shared";
import { logger } from "../logger";
import { GameAchievement } from "@main/entity";
export const getGameAchievementData = async (
objectId: string,
shop: GameShop,
useCachedData: boolean
cachedAchievements: GameAchievement | null
) => {
if (useCachedData) {
const cachedAchievements = await gameAchievementRepository.findOne({
where: { objectId, shop },
});
if (cachedAchievements && cachedAchievements.achievements) {
return JSON.parse(cachedAchievements.achievements) as AchievementData[];
}
if (cachedAchievements && cachedAchievements.achievements) {
return JSON.parse(cachedAchievements.achievements) as AchievementData[];
}
const userPreferences = await userPreferencesRepository.findOne({

View file

@ -7,8 +7,9 @@ import { WindowManager } from "../window-manager";
import { HydraApi } from "../hydra-api";
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
import { Game } from "@main/entity";
import { achievementsLogger } from "../logger";
import { publishNewAchievementNotification } from "../notifications";
import { SubscriptionRequiredError } from "@shared";
import { achievementsLogger } from "../logger";
const saveAchievementsOnLocal = async (
objectId: string,
@ -120,10 +121,14 @@ export const mergeAchievements = async (
}
if (game.remoteId) {
await HydraApi.put("/profile/games/achievements", {
id: game.remoteId,
achievements: mergedLocalAchievements,
})
await HydraApi.put(
"/profile/games/achievements",
{
id: game.remoteId,
achievements: mergedLocalAchievements,
},
{ needsSubscription: !newAchievements.length }
)
.then((response) => {
return saveAchievementsOnLocal(
response.objectId,
@ -133,7 +138,13 @@ export const mergeAchievements = async (
);
})
.catch((err) => {
achievementsLogger.error(err);
if (err! instanceof SubscriptionRequiredError) {
achievementsLogger.log(
"Achievements not synchronized on API due to lack of subscription",
game.objectID,
game.title
);
}
return saveAchievementsOnLocal(
game.objectID,

View file

@ -9,144 +9,134 @@ export const parseAchievementFile = (
): UnlockedAchievement[] => {
if (!existsSync(filePath)) return [];
if (type == Cracker.codex) {
const parsed = iniParse(filePath);
return processDefault(parsed);
try {
if (type == Cracker.codex) {
const parsed = iniParse(filePath);
return processDefault(parsed);
}
if (type == Cracker.rune) {
const parsed = iniParse(filePath);
return processDefault(parsed);
}
if (type === Cracker.onlineFix) {
const parsed = iniParse(filePath);
return processOnlineFix(parsed);
}
if (type === Cracker.goldberg) {
const parsed = jsonParse(filePath);
return processGoldberg(parsed);
}
if (type == Cracker.userstats) {
const parsed = iniParse(filePath);
return processUserStats(parsed);
}
if (type == Cracker.rld) {
const parsed = iniParse(filePath);
return processRld(parsed);
}
if (type === Cracker.skidrow) {
const parsed = iniParse(filePath);
return processSkidrow(parsed);
}
if (type === Cracker._3dm) {
const parsed = iniParse(filePath);
return process3DM(parsed);
}
if (type === Cracker.flt) {
const achievements = readdirSync(filePath);
return achievements.map((achievement) => {
return {
name: achievement,
unlockTime: Date.now(),
};
});
}
if (type === Cracker.creamAPI) {
const parsed = iniParse(filePath);
return processCreamAPI(parsed);
}
if (type === Cracker.empress) {
const parsed = jsonParse(filePath);
return processGoldberg(parsed);
}
if (type === Cracker.razor1911) {
return processRazor1911(filePath);
}
achievementsLogger.log(
`Unprocessed ${type} achievements found on ${filePath}`
);
return [];
} catch (err) {
achievementsLogger.error(`Error parsing ${type} - ${filePath}`, err);
return [];
}
if (type == Cracker.rune) {
const parsed = iniParse(filePath);
return processDefault(parsed);
}
if (type === Cracker.onlineFix) {
const parsed = iniParse(filePath);
return processOnlineFix(parsed);
}
if (type === Cracker.goldberg) {
const parsed = jsonParse(filePath);
return processGoldberg(parsed);
}
if (type == Cracker.userstats) {
const parsed = iniParse(filePath);
return processUserStats(parsed);
}
if (type == Cracker.rld) {
const parsed = iniParse(filePath);
return processRld(parsed);
}
if (type === Cracker.skidrow) {
const parsed = iniParse(filePath);
return processSkidrow(parsed);
}
if (type === Cracker._3dm) {
const parsed = iniParse(filePath);
return process3DM(parsed);
}
if (type === Cracker.flt) {
const achievements = readdirSync(filePath);
return achievements.map((achievement) => {
return {
name: achievement,
unlockTime: Date.now(),
};
});
}
if (type === Cracker.creamAPI) {
const parsed = iniParse(filePath);
return processCreamAPI(parsed);
}
if (type === Cracker.empress) {
const parsed = jsonParse(filePath);
return processGoldberg(parsed);
}
if (type === Cracker.razor1911) {
return processRazor1911(filePath);
}
achievementsLogger.log(
`Unprocessed ${type} achievements found on ${filePath}`
);
return [];
};
const iniParse = (filePath: string) => {
try {
const fileContent = readFileSync(filePath, "utf-8");
const fileContent = readFileSync(filePath, "utf-8");
const lines =
fileContent.charCodeAt(0) === 0xfeff
? fileContent.slice(1).split(/[\r\n]+/)
: fileContent.split(/[\r\n]+/);
const lines =
fileContent.charCodeAt(0) === 0xfeff
? fileContent.slice(1).split(/[\r\n]+/)
: fileContent.split(/[\r\n]+/);
let objectName = "";
const object: Record<string, Record<string, string | number>> = {};
let objectName = "";
const object: Record<string, Record<string, string | number>> = {};
for (const line of lines) {
if (line.startsWith("###") || !line.length) continue;
for (const line of lines) {
if (line.startsWith("###") || !line.length) continue;
if (line.startsWith("[") && line.endsWith("]")) {
objectName = line.slice(1, -1);
object[objectName] = {};
} else {
const [name, ...value] = line.split("=");
object[objectName][name.trim()] = value.join("=").trim();
}
if (line.startsWith("[") && line.endsWith("]")) {
objectName = line.slice(1, -1);
object[objectName] = {};
} else {
const [name, ...value] = line.split("=");
object[objectName][name.trim()] = value.join("=").trim();
}
return object;
} catch (err) {
achievementsLogger.error(`Error parsing ${filePath}`, err);
return {};
}
return object;
};
const jsonParse = (filePath: string) => {
try {
return JSON.parse(readFileSync(filePath, "utf-8"));
} catch (err) {
achievementsLogger.error(`Error parsing ${filePath}`, err);
return {};
}
return JSON.parse(readFileSync(filePath, "utf-8"));
};
const processRazor1911 = (filePath: string): UnlockedAchievement[] => {
try {
const fileContent = readFileSync(filePath, "utf-8");
const fileContent = readFileSync(filePath, "utf-8");
const lines =
fileContent.charCodeAt(0) === 0xfeff
? fileContent.slice(1).split(/[\r\n]+/)
: fileContent.split(/[\r\n]+/);
const lines =
fileContent.charCodeAt(0) === 0xfeff
? fileContent.slice(1).split(/[\r\n]+/)
: fileContent.split(/[\r\n]+/);
const achievements: UnlockedAchievement[] = [];
for (const line of lines) {
if (!line.length) continue;
const achievements: UnlockedAchievement[] = [];
for (const line of lines) {
if (!line.length) continue;
const [name, unlocked, unlockTime] = line.split(" ");
if (unlocked === "1") {
achievements.push({
name,
unlockTime: Number(unlockTime) * 1000,
});
}
const [name, unlocked, unlockTime] = line.split(" ");
if (unlocked === "1") {
achievements.push({
name,
unlockTime: Number(unlockTime) * 1000,
});
}
return achievements;
} catch (err) {
achievementsLogger.error(`Error processing ${filePath}`, err);
return [];
}
return achievements;
};
const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {

View file

@ -0,0 +1,30 @@
import path from "node:path";
import cp from "node:child_process";
import { app } from "electron";
export const startAria2 = () => {};
export class Aria2 {
private static process: cp.ChildProcess | null = null;
public static spawn() {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
this.process = cp.spawn(
binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
],
{ stdio: "inherit", windowsHide: true }
);
}
public static kill() {
this.process?.kill();
}
}

View file

@ -1,39 +1,116 @@
import { Game } from "@main/entity";
import { Downloader } from "@shared";
import { PythonInstance } from "./python-instance";
import { WindowManager } from "../window-manager";
import { downloadQueueRepository, gameRepository } from "@main/repository";
import {
downloadQueueRepository,
gameRepository,
userPreferencesRepository,
} from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types";
import { GofileApi, QiwiApi } from "../hosters";
import { GenericHttpDownloader } from "./generic-http-downloader";
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
import { PythonRPC } from "../python-rpc";
import {
LibtorrentPayload,
LibtorrentStatus,
PauseDownloadPayload,
} from "./types";
import { calculateETA, getDirSize } from "./helpers";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { RealDebridClient } from "./real-debrid";
import path from "path";
import { logger } from "../logger";
export class DownloadManager {
private static currentDownloader: Downloader | null = null;
private static downloadingGameId: number | null = null;
public static async watchDownloads() {
let status: DownloadProgress | null = null;
public static async startRPC(game?: Game, initialSeeding?: Game[]) {
PythonRPC.spawn(
game?.status === "active"
? await this.getDownloadPayload(game).catch(() => undefined)
: undefined,
initialSeeding?.map((game) => ({
game_id: game.id,
url: game.uri!,
save_path: game.downloadPath!,
}))
);
if (this.currentDownloader === Downloader.Torrent) {
status = await PythonInstance.getStatus();
} else if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus();
} else {
status = await GenericHttpDownloader.getStatus();
this.downloadingGameId = game?.id ?? null;
}
private static async getDownloadStatus() {
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
"/status"
);
if (response.data === null || !this.downloadingGameId) return null;
const gameId = this.downloadingGameId;
try {
const {
progress,
numPeers,
numSeeds,
downloadSpeed,
bytesDownloaded,
fileSize,
folderName,
status,
} = response.data;
const isDownloadingMetadata =
status === LibtorrentStatus.DownloadingMetadata;
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
if (!isDownloadingMetadata && !isCheckingFiles) {
const update: QueryDeepPartialEntity<Game> = {
bytesDownloaded,
fileSize,
progress,
status: "active",
};
await gameRepository.update(
{ id: gameId },
{
...update,
folderName,
}
);
}
return {
numPeers,
numSeeds,
downloadSpeed,
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
isDownloadingMetadata,
isCheckingFiles,
progress,
gameId,
} as DownloadProgress;
} catch (err) {
return null;
}
}
public static async watchDownloads() {
const status = await this.getDownloadStatus();
if (status) {
const { gameId, progress } = status;
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
const userPreferences = await userPreferencesRepository.findOneBy({
id: 1,
});
if (WindowManager.mainWindow && game) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(
@ -44,12 +121,27 @@ export class DownloadManager {
)
);
}
if (progress === 1 && game) {
publishDownloadCompleteNotification(game);
await downloadQueueRepository.delete({ game });
if (
userPreferences?.seedAfterDownloadComplete &&
game.downloader === Downloader.Torrent
) {
gameRepository.update(
{ id: gameId },
{ status: "seeding", shouldSeed: true }
);
} else {
gameRepository.update(
{ id: gameId },
{ status: "complete", shouldSeed: false }
);
this.cancelDownload(gameId);
}
await downloadQueueRepository.delete({ game });
const [nextQueueItem] = await downloadQueueRepository.find({
order: {
id: "DESC",
@ -58,25 +150,61 @@ export class DownloadManager {
game: true,
},
});
if (nextQueueItem) {
this.resumeDownload(nextQueueItem.game);
} else {
this.downloadingGameId = -1;
}
}
}
}
public static async getSeedStatus() {
const seedStatus = await PythonRPC.rpc
.get<LibtorrentPayload[] | []>("/seed-status")
.then((res) => res.data);
if (!seedStatus.length) return;
logger.log(seedStatus);
seedStatus.forEach(async (status) => {
const game = await gameRepository.findOne({
where: { id: status.gameId },
});
if (!game) return;
const totalSize = await getDirSize(
path.join(game.downloadPath!, status.folderName)
);
if (totalSize < status.fileSize) {
await this.cancelDownload(game.id);
await gameRepository.update(game.id, {
status: "paused",
shouldSeed: false,
progress: totalSize / status.fileSize,
});
WindowManager.mainWindow?.webContents.send("on-hard-delete");
}
});
WindowManager.mainWindow?.webContents.send("on-seeding-status", seedStatus);
}
static async pauseDownload() {
if (this.currentDownloader === Downloader.Torrent) {
await PythonInstance.pauseDownload();
} else if (this.currentDownloader === Downloader.RealDebrid) {
await RealDebridDownloader.pauseDownload();
} else {
await GenericHttpDownloader.pauseDownload();
}
await PythonRPC.rpc
.post("/action", {
action: "pause",
game_id: this.downloadingGameId,
} as PauseDownloadPayload)
.catch(() => {});
WindowManager.mainWindow?.setProgressBar(-1);
this.currentDownloader = null;
this.downloadingGameId = null;
}
@ -85,55 +213,105 @@ export class DownloadManager {
}
static async cancelDownload(gameId = this.downloadingGameId!) {
if (this.currentDownloader === Downloader.Torrent) {
PythonInstance.cancelDownload(gameId);
} else if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload(gameId);
} else {
GenericHttpDownloader.cancelDownload(gameId);
}
await PythonRPC.rpc.post("/action", {
action: "cancel",
game_id: gameId,
});
WindowManager.mainWindow?.setProgressBar(-1);
this.currentDownloader = null;
this.downloadingGameId = null;
if (gameId === this.downloadingGameId) {
this.downloadingGameId = null;
}
}
static async startDownload(game: Game) {
static async resumeSeeding(game: Game) {
await PythonRPC.rpc.post("/action", {
action: "resume_seeding",
game_id: game.id,
url: game.uri,
save_path: game.downloadPath,
});
}
static async pauseSeeding(gameId: number) {
await PythonRPC.rpc.post("/action", {
action: "pause_seeding",
game_id: gameId,
});
}
private static async getDownloadPayload(game: Game) {
switch (game.downloader) {
case Downloader.Gofile: {
const id = game!.uri!.split("/").pop();
const id = game.uri!.split("/").pop();
const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!);
GenericHttpDownloader.startDownload(game, downloadLink, {
Cookie: `accountToken=${token}`,
});
break;
return {
action: "start",
game_id: game.id,
url: downloadLink,
save_path: game.downloadPath!,
header: `Cookie: accountToken=${token}`,
};
}
case Downloader.PixelDrain: {
const id = game!.uri!.split("/").pop();
const id = game.uri!.split("/").pop();
await GenericHttpDownloader.startDownload(
game,
`https://pixeldrain.com/api/file/${id}?download`
);
break;
return {
action: "start",
game_id: game.id,
url: `https://pixeldrain.com/api/file/${id}?download`,
save_path: game.downloadPath!,
};
}
case Downloader.Qiwi: {
const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
await GenericHttpDownloader.startDownload(game, downloadUrl);
break;
return {
action: "start",
game_id: game.id,
url: downloadUrl,
save_path: game.downloadPath!,
};
}
case Downloader.Datanodes: {
const downloadUrl = await DatanodesApi.getDownloadUrl(game.uri!);
return {
action: "start",
game_id: game.id,
url: downloadUrl,
save_path: game.downloadPath!,
};
}
case Downloader.Torrent:
PythonInstance.startDownload(game);
break;
case Downloader.RealDebrid:
RealDebridDownloader.startDownload(game);
}
return {
action: "start",
game_id: game.id,
url: game.uri!,
save_path: game.downloadPath!,
};
case Downloader.RealDebrid: {
const downloadUrl = await RealDebridClient.getDownloadUrl(game.uri!);
return {
action: "start",
game_id: game.id,
url: downloadUrl!,
save_path: game.downloadPath!,
};
}
}
}
static async startDownload(game: Game) {
const payload = await this.getDownloadPayload(game);
await PythonRPC.rpc.post("/action", payload);
this.currentDownloader = game.downloader;
this.downloadingGameId = game.id;
}
}

View file

@ -1,109 +0,0 @@
import { Game } from "@main/entity";
import { gameRepository } from "@main/repository";
import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download";
export class GenericHttpDownloader {
public static downloads = new Map<number, HttpDownload>();
public static downloadingGame: Game | null = null;
public static async getStatus() {
if (this.downloadingGame) {
const download = this.downloads.get(this.downloadingGame.id)!;
const status = download.getStatus();
if (status) {
const progress =
Number(status.completedLength) / Number(status.totalLength);
await gameRepository.update(
{ id: this.downloadingGame!.id },
{
bytesDownloaded: Number(status.completedLength),
fileSize: Number(status.totalLength),
progress,
status: "active",
folderName: status.folderName,
}
);
const result = {
numPeers: 0,
numSeeds: 0,
downloadSpeed: status.downloadSpeed,
timeRemaining: calculateETA(
status.totalLength,
status.completedLength,
status.downloadSpeed
),
isDownloadingMetadata: false,
isCheckingFiles: false,
progress,
gameId: this.downloadingGame!.id,
} as DownloadProgress;
if (progress === 1) {
this.downloads.delete(this.downloadingGame.id);
this.downloadingGame = null;
}
return result;
}
}
return null;
}
static async pauseDownload() {
if (this.downloadingGame) {
const httpDownload = this.downloads.get(this.downloadingGame!.id!);
if (httpDownload) {
await httpDownload.pauseDownload();
}
this.downloadingGame = null;
}
}
static async startDownload(
game: Game,
downloadUrl: string,
headers?: Record<string, string>
) {
this.downloadingGame = game;
if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!);
return;
}
const httpDownload = new HttpDownload(
game.downloadPath!,
downloadUrl,
headers
);
httpDownload.startDownload();
this.downloads.set(game.id!, httpDownload);
}
static async cancelDownload(gameId: number) {
const httpDownload = this.downloads.get(gameId);
if (httpDownload) {
await httpDownload.cancelDownload();
this.downloads.delete(gameId);
}
}
static async resumeDownload(gameId: number) {
const httpDownload = this.downloads.get(gameId);
if (httpDownload) {
await httpDownload.resumeDownload();
}
}
}

View file

@ -1,3 +1,7 @@
import path from "node:path";
import fs from "node:fs";
import { logger } from "../logger";
export const calculateETA = (
totalLength: number,
completedLength: number,
@ -11,3 +15,26 @@ export const calculateETA = (
return -1;
};
export const getDirSize = async (dir: string): Promise<number> => {
const getItemSize = async (filePath: string): Promise<number> => {
const stat = await fs.promises.stat(filePath);
if (stat.isDirectory()) {
return getDirSize(filePath);
}
return stat.size;
};
try {
const files = await fs.promises.readdir(dir);
const filePaths = files.map((file) => path.join(dir, file));
const sizes = await Promise.all(filePaths.map(getItemSize));
return sizes.reduce((total, size) => total + size, 0);
} catch (error) {
logger.error(error);
return 0;
}
};

View file

@ -1,54 +0,0 @@
import { WindowManager } from "../window-manager";
import path from "node:path";
export class HttpDownload {
private downloadItem: Electron.DownloadItem;
constructor(
private downloadPath: string,
private downloadUrl: string,
private headers?: Record<string, string>
) {}
public getStatus() {
return {
completedLength: this.downloadItem.getReceivedBytes(),
totalLength: this.downloadItem.getTotalBytes(),
downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
folderName: this.downloadItem.getFilename(),
};
}
async cancelDownload() {
this.downloadItem.cancel();
}
async pauseDownload() {
this.downloadItem.pause();
}
async resumeDownload() {
this.downloadItem.resume();
}
async startDownload() {
return new Promise((resolve) => {
const options = this.headers ? { headers: this.headers } : {};
WindowManager.mainWindow?.webContents.downloadURL(
this.downloadUrl,
options
);
WindowManager.mainWindow?.webContents.session.once(
"will-download",
(_event, item, _webContents) => {
this.downloadItem = item;
item.setSavePath(path.join(this.downloadPath, item.getFilename()));
resolve(null);
}
);
});
}
}

View file

@ -1,2 +1 @@
export * from "./download-manager";
export * from "./python-instance";

View file

@ -1,188 +0,0 @@
import cp from "node:child_process";
import { Game } from "@main/entity";
import {
RPC_PASSWORD,
RPC_PORT,
startTorrentClient as startRPCClient,
} from "./torrent-client";
import { gameRepository } from "@main/repository";
import type { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { calculateETA } from "./helpers";
import axios from "axios";
import {
CancelDownloadPayload,
StartDownloadPayload,
PauseDownloadPayload,
LibtorrentStatus,
LibtorrentPayload,
ProcessPayload,
} from "./types";
import { pythonInstanceLogger as logger } from "../logger";
export class PythonInstance {
private static pythonProcess: cp.ChildProcess | null = null;
private static downloadingGameId = -1;
private static rpc = axios.create({
baseURL: `http://localhost:${RPC_PORT}`,
headers: {
"x-hydra-rpc-password": RPC_PASSWORD,
},
});
public static spawn(args?: StartDownloadPayload) {
logger.log("spawning python process with args:", args);
this.pythonProcess = startRPCClient(args);
}
public static kill() {
if (this.pythonProcess) {
logger.log("killing python process");
this.pythonProcess.kill();
this.pythonProcess = null;
this.downloadingGameId = -1;
}
}
public static killTorrent() {
if (this.pythonProcess) {
logger.log("killing torrent in python process");
this.rpc.post("/action", { action: "kill-torrent" });
this.downloadingGameId = -1;
}
}
public static async getProcessList() {
return (
(await this.rpc.get<ProcessPayload[] | null>("/process-list")).data || []
);
}
public static async getStatus() {
if (this.downloadingGameId === -1) return null;
const response = await this.rpc.get<LibtorrentPayload | null>("/status");
if (response.data === null) return null;
try {
const {
progress,
numPeers,
numSeeds,
downloadSpeed,
bytesDownloaded,
fileSize,
folderName,
status,
gameId,
} = response.data;
this.downloadingGameId = gameId;
const isDownloadingMetadata =
status === LibtorrentStatus.DownloadingMetadata;
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
if (!isDownloadingMetadata && !isCheckingFiles) {
const update: QueryDeepPartialEntity<Game> = {
bytesDownloaded,
fileSize,
progress,
status: "active",
};
await gameRepository.update(
{ id: gameId },
{
...update,
folderName,
}
);
}
if (progress === 1 && !isCheckingFiles) {
this.downloadingGameId = -1;
}
return {
numPeers,
numSeeds,
downloadSpeed,
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
isDownloadingMetadata,
isCheckingFiles,
progress,
gameId,
} as DownloadProgress;
} catch (err) {
return null;
}
}
static async pauseDownload() {
await this.rpc
.post("/action", {
action: "pause",
game_id: this.downloadingGameId,
} as PauseDownloadPayload)
.catch(() => {});
this.downloadingGameId = -1;
}
static async startDownload(game: Game) {
if (!this.pythonProcess) {
this.spawn({
game_id: game.id,
magnet: game.uri!,
save_path: game.downloadPath!,
});
} else {
await this.rpc
.post("/action", {
action: "start",
game_id: game.id,
magnet: game.uri,
save_path: game.downloadPath,
} as StartDownloadPayload)
.catch(this.handleRpcError);
}
this.downloadingGameId = game.id;
}
static async cancelDownload(gameId: number) {
await this.rpc
.post("/action", {
action: "cancel",
game_id: gameId,
} as CancelDownloadPayload)
.catch(() => {});
this.downloadingGameId = -1;
}
static async processProfileImage(imagePath: string) {
return this.rpc
.post<{ imagePath: string; mimeType: string }>("/profile-image", {
image_path: imagePath,
})
.then((response) => response.data);
}
private static async handleRpcError(error: unknown) {
logger.error(error);
return this.rpc.get("/healthcheck").catch(() => {
logger.error(
"RPC healthcheck failed. Killing process and starting again"
);
this.kill();
this.spawn();
});
}
}

View file

@ -1,72 +0,0 @@
import { Game } from "@main/entity";
import { RealDebridClient } from "../real-debrid";
import { HttpDownload } from "./http-download";
import { GenericHttpDownloader } from "./generic-http-downloader";
export class RealDebridDownloader extends GenericHttpDownloader {
private static realDebridTorrentId: string | null = null;
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
let torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
if (torrentInfo.status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
}
const { links, status } = torrentInfo;
if (status === "downloaded") {
const [link] = links;
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
return null;
}
if (this.downloadingGame?.uri) {
const { download } = await RealDebridClient.unrestrictLink(
this.downloadingGame?.uri
);
return decodeURIComponent(download);
}
return null;
}
static async startDownload(game: Game) {
if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!);
this.downloadingGame = game;
return;
}
if (game.uri?.startsWith("magnet:")) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
game!.uri!
);
}
this.downloadingGame = game;
const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) {
this.realDebridTorrentId = null;
const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
httpDownload.startDownload();
this.downloads.set(game.id!, httpDownload);
}
}
}

View file

@ -9,7 +9,7 @@ import type {
export class RealDebridClient {
private static instance: AxiosInstance;
private static baseURL = "https://api.real-debrid.com/rest/1.0";
private static readonly baseURL = "https://api.real-debrid.com/rest/1.0";
static authorize(apiToken: string) {
this.instance = axios.create({
@ -83,4 +83,37 @@ export class RealDebridClient {
const torrent = await RealDebridClient.addMagnet(magnetUri);
return torrent.id;
}
public static async getDownloadUrl(uri: string) {
let realDebridTorrentId: string | null = null;
if (uri.startsWith("magnet:")) {
realDebridTorrentId = await this.getTorrentId(uri);
}
if (realDebridTorrentId) {
let torrentInfo = await this.getTorrentInfo(realDebridTorrentId);
if (torrentInfo.status === "waiting_files_selection") {
await this.selectAllFiles(realDebridTorrentId);
torrentInfo = await this.getTorrentInfo(realDebridTorrentId);
}
const { links, status } = torrentInfo;
if (status === "downloaded") {
const [link] = links;
const { download } = await this.unrestrictLink(link);
return decodeURIComponent(download);
}
return null;
}
const { download } = await this.unrestrictLink(uri);
return decodeURIComponent(download);
}
}

View file

@ -0,0 +1,97 @@
import axios, { AxiosInstance } from "axios";
import parseTorrent from "parse-torrent";
import type {
TorBoxUserRequest,
TorBoxTorrentInfoRequest,
TorBoxAddTorrentRequest,
TorBoxRequestLinkRequest,
} from "@types";
import { logger } from "../logger";
export class TorBoxClient {
private static instance: AxiosInstance;
private static readonly baseURL = "https://api.torbox.app/v1/api";
public static apiToken: string;
static authorize(apiToken: string) {
this.instance = axios.create({
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
this.apiToken = apiToken;
}
static async addMagnet(magnet: string) {
const form = new FormData();
form.append("magnet", magnet);
const response = await this.instance.post<TorBoxAddTorrentRequest>(
"/torrents/createtorrent",
form
);
return response.data.data;
}
static async getTorrentInfo(id: number) {
const response =
await this.instance.get<TorBoxTorrentInfoRequest>("/torrents/mylist");
const data = response.data.data;
const info = data.find((item) => item.id === id);
if (!info) {
return null;
}
return info;
}
static async getUser() {
const response = await this.instance.get<TorBoxUserRequest>(`/user/me`);
return response.data.data;
}
static async requestLink(id: number) {
const searchParams = new URLSearchParams({});
searchParams.set("token", this.apiToken);
searchParams.set("torrent_id", id.toString());
searchParams.set("zip_link", "true");
const response = await this.instance.get<TorBoxRequestLinkRequest>(
"/torrents/requestdl?" + searchParams.toString()
);
if (response.status !== 200) {
logger.error(response.data.error);
logger.error(response.data.detail);
return null;
}
return response.data.data;
}
private static async getAllTorrentsFromUser() {
const response =
await this.instance.get<TorBoxTorrentInfoRequest>("/torrents/mylist");
return response.data.data;
}
static async getTorrentId(magnetUri: string) {
const userTorrents = await this.getAllTorrentsFromUser();
const { infoHash } = await parseTorrent(magnetUri);
const userTorrent = userTorrents.find(
(userTorrent) => userTorrent.hash === infoHash
);
if (userTorrent) return userTorrent.id;
const torrent = await this.addMagnet(magnetUri);
return torrent.torrent_id;
}
}

View file

@ -1,77 +0,0 @@
import path from "node:path";
import cp from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import { app, dialog } from "electron";
import type { StartDownloadPayload } from "./types";
import { Readable } from "node:stream";
import { pythonInstanceLogger as logger } from "../logger";
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
darwin: "hydra-download-manager",
linux: "hydra-download-manager",
win32: "hydra-download-manager.exe",
};
export const BITTORRENT_PORT = "5881";
export const RPC_PORT = "8084";
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
const logStderr = (readable: Readable | null) => {
if (!readable) return;
readable.setEncoding("utf-8");
readable.on("data", logger.log);
};
export const startTorrentClient = (args?: StartDownloadPayload) => {
const commonArgs = [
BITTORRENT_PORT,
RPC_PORT,
RPC_PASSWORD,
args ? encodeURIComponent(JSON.stringify(args)) : "",
];
if (app.isPackaged) {
const binaryName = binaryNameByPlatform[process.platform]!;
const binaryPath = path.join(
process.resourcesPath,
"hydra-download-manager",
binaryName
);
if (!fs.existsSync(binaryPath)) {
dialog.showErrorBox(
"Fatal",
"Hydra Download Manager binary not found. Please check if it has been removed by Windows Defender."
);
app.quit();
}
const childProcess = cp.spawn(binaryPath, commonArgs, {
windowsHide: true,
stdio: ["inherit", "inherit"],
});
logStderr(childProcess.stderr);
return childProcess;
} else {
const scriptPath = path.join(
__dirname,
"..",
"..",
"torrent-client",
"main.py"
);
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
stdio: ["inherit", "inherit"],
});
logStderr(childProcess.stderr);
return childProcess;
}
};

View file

@ -1,9 +1,3 @@
export interface StartDownloadPayload {
game_id: number;
magnet: string;
save_path: string;
}
export interface PauseDownloadPayload {
game_id: number;
}
@ -25,6 +19,7 @@ export interface LibtorrentPayload {
numPeers: number;
numSeeds: number;
downloadSpeed: number;
uploadSpeed: number;
bytesDownloaded: number;
fileSize: number;
folderName: string;
@ -33,7 +28,15 @@ export interface LibtorrentPayload {
}
export interface ProcessPayload {
exe: string;
exe: string | null;
pid: number;
name: string;
}
export interface PauseSeedingPayload {
game_id: number;
}
export interface ResumeSeedingPayload {
game_id: number;
}

View file

@ -0,0 +1,46 @@
import axios, { AxiosResponse } from "axios";
export class DatanodesApi {
private static readonly session = axios.create({});
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
const parsedUrl = new URL(downloadUrl);
const pathSegments = parsedUrl.pathname.split("/");
const fileCode = decodeURIComponent(pathSegments[1]);
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
const payload = new URLSearchParams({
op: "download2",
id: fileCode,
rand: "",
referer: "https://datanodes.to/download",
method_free: "Free Download >>",
method_premium: "",
adblock_detected: "",
});
const response: AxiosResponse = await this.session.post(
"https://datanodes.to/download",
payload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
Host: "datanodes.to",
Origin: "https://datanodes.to",
Referer: "https://datanodes.to/download",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
},
maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400,
}
);
if (response.status === 302) {
return response.headers["location"];
}
return "";
}
}

View file

@ -1,2 +1,3 @@
export * from "./gofile";
export * from "./qiwi";
export * from "./datanodes";

View file

@ -23,7 +23,7 @@ interface HydraApiUserAuth {
authToken: string;
refreshToken: string;
expirationTimestamp: number;
subscription: { expiresAt: Date | null } | null;
subscription: { expiresAt: Date | string | null } | null;
}
export class HydraApi {
@ -159,7 +159,11 @@ export class HydraApi {
config.method,
config.baseURL,
config.url,
omit(config.headers, ["accessToken", "refreshToken"]),
omit(config.headers, [
"accessToken",
"refreshToken",
"Authorization",
]),
Array.isArray(data)
? data
: omit(data, ["accessToken", "refreshToken"])
@ -182,8 +186,6 @@ export class HydraApi {
);
}
await getUserData();
const userAuth = await userAuthRepository.findOne({
where: { id: 1 },
relations: { subscription: true },
@ -197,6 +199,14 @@ export class HydraApi {
? { expiresAt: userAuth.subscription?.expiresAt }
: null,
};
const updatedUserData = await getUserData();
this.userAuth.subscription = updatedUserData?.subscription
? {
expiresAt: updatedUserData.subscription.expiresAt,
}
: null;
}
private static sendSignOutEvent() {
@ -284,10 +294,8 @@ export class HydraApi {
await this.revalidateAccessTokenIfExpired();
}
if (needsSubscription) {
if (!(await this.hasActiveSubscription())) {
throw new SubscriptionRequiredError();
}
if (needsSubscription && !this.hasActiveSubscription()) {
throw new SubscriptionRequiredError();
}
}

View file

@ -1,7 +1,6 @@
export * from "./logger";
export * from "./steam";
export * from "./steam-250";
export * from "./steam-grid";
export * from "./window-manager";
export * from "./download";
export * from "./process-watcher";

View file

@ -31,6 +31,6 @@ log.errorHandler.startCatching({
log.initialize();
export const pythonInstanceLogger = log.scope("python-instance");
export const pythonRpcLogger = log.scope("python-rpc");
export const logger = log.scope("main");
export const achievementsLogger = log.scope("achievements");

View file

@ -10,6 +10,7 @@ export const startMainLoop = async () => {
watchProcesses(),
DownloadManager.watchDownloads(),
AchievementWatcherManager.watchAchievements(),
DownloadManager.getSeedStatus(),
]);
await sleep(1500);

View file

@ -1,53 +1,190 @@
import { IsNull, Not } from "typeorm";
import { gameRepository } from "@main/repository";
import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
import type { GameRunning } from "@types";
import { PythonInstance } from "./download";
import { PythonRPC } from "./python-rpc";
import { Game } from "@main/entity";
import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
const commands = {
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
findWineExecutables: `lsof -c wine 2>/dev/null | grep '\\.exe$' | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
};
export const gamesPlaytime = new Map<
number,
{ lastTick: number; firstTick: number; lastSyncTick: number }
>();
interface ExecutableInfo {
name: string;
os: string;
exe: string;
}
interface GameExecutables {
[key: string]: ExecutableInfo[];
}
const TICKS_TO_UPDATE_API = 120;
let currentTick = 1;
const getSystemProcessSet = async () => {
const processes = await PythonInstance.getProcessList();
const isWindowsPlatform = process.platform === "win32";
const isLinuxPlatform = process.platform === "linux";
if (process.platform === "linux")
return new Set(processes.map((process) => process.name));
return new Set(processes.map((process) => process.exe));
const getGameExecutables = async () => {
const gameExecutables = (
await axios
.get(
import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL +
"/game-executables.json"
)
.catch(() => {
return { data: {} };
})
).data as GameExecutables;
Object.keys(gameExecutables).forEach((key) => {
gameExecutables[key] = gameExecutables[key]
.filter((executable) => {
if (isWindowsPlatform) {
return executable.os === "win32";
} else if (isLinuxPlatform) {
return executable.os === "linux" || executable.os === "win32";
}
return false;
})
.map((executable) => {
return {
name: isWindowsPlatform
? executable.name.replace(/\//g, "\\")
: executable.name,
os: executable.os,
exe: executable.name.slice(executable.name.lastIndexOf("/") + 1),
};
});
});
return gameExecutables;
};
const getExecutable = (game: Game) => {
if (process.platform === "linux")
return game.executablePath?.split("/").at(-1);
return game.executablePath;
const gameExecutables = await getGameExecutables();
const findGamePathByProcess = (
processMap: Map<string, Set<string>>,
gameId: string
) => {
const executables = gameExecutables[gameId];
for (const executable of executables) {
const pathSet = processMap.get(executable.exe);
if (pathSet) {
pathSet.forEach((path) => {
if (path.toLowerCase().endsWith(executable.name)) {
gameRepository.update(
{ objectID: gameId, shop: "steam" },
{ executablePath: path }
);
if (isLinuxPlatform) {
exec(commands.findWineDir, (err, out) => {
if (err) return;
gameRepository.update(
{ objectID: gameId, shop: "steam" },
{
winePrefixPath: out.trim().replace("/drive_c/windows", ""),
}
);
});
}
}
});
}
}
};
const getSystemProcessMap = async () => {
const processes =
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
[];
const map = new Map<string, Set<string>>();
processes.forEach((process) => {
const key = process.name?.toLowerCase();
const value = process.exe;
if (!key || !value) return;
const currentSet = map.get(key) ?? new Set();
map.set(key, currentSet.add(value));
});
if (isLinuxPlatform) {
await new Promise((res) => {
exec(commands.findWineExecutables, (err, out) => {
if (err) {
res(null);
return;
}
const pathSet = new Set(
out
.trim()
.split("\n")
.map((path) => path.trim())
);
pathSet.forEach((path) => {
if (path.startsWith("/usr")) return;
const key = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
if (!key || !path) return;
const currentSet = map.get(key) ?? new Set();
map.set(key, currentSet.add(path));
});
res(null);
});
});
}
return map;
};
export const watchProcesses = async () => {
const games = await gameRepository.find({
where: {
executablePath: Not(IsNull()),
isDeleted: false,
},
});
if (games.length === 0) return;
if (!games.length) return;
const processSet = await getSystemProcessSet();
const processMap = await getSystemProcessMap();
for (const game of games) {
const executable = getExecutable(game);
const executablePath = game.executablePath;
if (!executablePath) {
if (gameExecutables[game.objectID]) {
findGamePathByProcess(processMap, game.objectID);
}
continue;
}
if (!executable) continue;
const executable = executablePath
.slice(executablePath.lastIndexOf(isWindowsPlatform ? "\\" : "/") + 1)
.toLowerCase();
const gameProcess = processSet.has(executable);
const hasProcess = processMap.get(executable)?.has(executablePath);
if (gameProcess) {
if (hasProcess) {
if (gamesPlaytime.has(game.id)) {
onTickGame(game);
} else {

View file

@ -0,0 +1,108 @@
import axios from "axios";
import cp from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { pythonRpcLogger } from "./logger";
import { Readable } from "node:stream";
import { app, dialog } from "electron";
interface GamePayload {
game_id: number;
url: string;
save_path: string;
}
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
darwin: "hydra-python-rpc",
linux: "hydra-python-rpc",
win32: "hydra-python-rpc.exe",
};
export class PythonRPC {
public static readonly BITTORRENT_PORT = "5881";
public static readonly RPC_PORT = "8084";
private static readonly RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
private static pythonProcess: cp.ChildProcess | null = null;
public static readonly rpc = axios.create({
baseURL: `http://localhost:${this.RPC_PORT}`,
headers: {
"x-hydra-rpc-password": this.RPC_PASSWORD,
},
});
private static logStderr(readable: Readable | null) {
if (!readable) return;
readable.setEncoding("utf-8");
readable.on("data", pythonRpcLogger.log);
}
public static spawn(
initialDownload?: GamePayload,
initialSeeding?: GamePayload[]
) {
const commonArgs = [
this.BITTORRENT_PORT,
this.RPC_PORT,
this.RPC_PASSWORD,
initialDownload ? JSON.stringify(initialDownload) : "",
initialSeeding ? JSON.stringify(initialSeeding) : "",
];
if (app.isPackaged) {
const binaryName = binaryNameByPlatform[process.platform]!;
const binaryPath = path.join(
process.resourcesPath,
"hydra-python-rpc",
binaryName
);
if (!fs.existsSync(binaryPath)) {
dialog.showErrorBox(
"Fatal",
"Hydra Python Instance binary not found. Please check if it has been removed by Windows Defender."
);
app.quit();
}
const childProcess = cp.spawn(binaryPath, commonArgs, {
windowsHide: true,
stdio: ["inherit", "inherit"],
});
this.logStderr(childProcess.stderr);
this.pythonProcess = childProcess;
} else {
const scriptPath = path.join(
__dirname,
"..",
"..",
"python_rpc",
"main.py"
);
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
stdio: ["inherit", "inherit"],
});
this.logStderr(childProcess.stderr);
this.pythonProcess = childProcess;
}
}
public static kill() {
if (this.pythonProcess) {
pythonRpcLogger.log("Killing python process");
this.pythonProcess.kill();
this.pythonProcess = null;
}
}
}

View file

@ -1,69 +0,0 @@
import type { GameShop } from "@types";
import axios from "axios";
export interface SteamGridResponse {
success: boolean;
data: {
id: number;
};
}
export interface SteamGridGameResponse {
data: {
platforms: {
steam: {
metadata: {
clienticon: string;
};
};
};
};
}
export const getSteamGridData = async (
objectId: string,
path: string,
shop: GameShop,
params: Record<string, string> = {}
): Promise<SteamGridResponse> => {
const searchParams = new URLSearchParams(params);
if (!import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY) {
throw new Error("MAIN_VITE_STEAMGRIDDB_API_KEY is not set");
}
const response = await axios.get(
`https://www.steamgriddb.com/api/v2/${path}/${shop}/${objectId}?${searchParams.toString()}`,
{
headers: {
Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`,
},
}
);
return response.data;
};
export const getSteamGridGameById = async (
id: number
): Promise<SteamGridGameResponse> => {
const response = await axios.get(
`https://www.steamgriddb.com/api/public/game/${id}`,
{
headers: {
Referer: "https://www.steamgriddb.com/",
},
}
);
return response.data;
};
export const getSteamGameClientIcon = async (objectId: string) => {
const {
data: { id: steamGridGameId },
} = await getSteamGridData(objectId, "games", "steam");
const steamGridGame = await getSteamGridGameById(steamGridGameId);
return steamGridGame.data.platforms.steam.metadata.clienticon;
};

View file

@ -42,6 +42,7 @@ export const getUserData = () => {
})
.catch(async (err) => {
if (err instanceof UserNotLoggedInError) {
logger.info("User is not logged in", err);
return null;
}
logger.error("Failed to get logged user");
@ -58,6 +59,9 @@ export const getUserData = () => {
bio: "",
email: null,
profileVisibility: "PUBLIC" as ProfileVisibility,
quirks: {
backupsPerGameLimit: 0,
},
subscription: loggedUser.subscription
? {
id: loggedUser.subscription.subscriptionId,

View file

@ -64,7 +64,10 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) {
if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot")
) {
return callback(details);
}
@ -81,15 +84,11 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) {
return callback(details);
}
if (details.url.includes("featurebase")) {
return callback(details);
}
if (details.url.includes("chatwoot")) {
if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("featurebase") ||
details.url.includes("chatwoot")
) {
return callback(details);
}
@ -277,14 +276,9 @@ export class WindowManager {
if (process.platform !== "darwin") {
await updateSystemTray();
tray.addListener("click", () => {
tray.addListener("double-click", () => {
if (this.mainWindow) {
if (
WindowManager.mainWindow?.isMinimized() ||
!WindowManager.mainWindow?.isVisible()
) {
WindowManager.mainWindow?.show();
}
this.mainWindow.show();
} else {
this.createMainWindow();
}

View file

@ -1,11 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
readonly MAIN_VITE_API_URL: string;
readonly MAIN_VITE_ANALYTICS_API_URL: string;
readonly MAIN_VITE_AUTH_URL: string;
readonly MAIN_VITE_CHECKOUT_URL: string;
readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string;
}
interface ImportMeta {

View file

@ -11,10 +11,12 @@ import type {
GameRunning,
FriendRequestAction,
UpdateProfileRequest,
CatalogueSearchPayload,
SeedingStatus,
GameAchievement,
} from "@types";
import type { CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios";
import { GameAchievement } from "@main/entity";
contextBridge.exposeInMainWorld("electron", {
/* Torrenting */
@ -26,6 +28,10 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("pauseGameDownload", gameId),
resumeGameDownload: (gameId: number) =>
ipcRenderer.invoke("resumeGameDownload", gameId),
pauseGameSeed: (gameId: number) =>
ipcRenderer.invoke("pauseGameSeed", gameId),
resumeGameSeed: (gameId: number) =>
ipcRenderer.invoke("resumeGameSeed", gameId),
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
@ -34,9 +40,23 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-download-progress", listener);
return () => ipcRenderer.removeListener("on-download-progress", listener);
},
onHardDelete: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-hard-delete", listener);
return () => ipcRenderer.removeListener("on-hard-delete", listener);
},
onSeedingStatus: (cb: (value: SeedingStatus[]) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
value: SeedingStatus[]
) => cb(value);
ipcRenderer.on("on-seeding-status", listener);
return () => ipcRenderer.removeListener("on-seeding-status", listener);
},
/* Catalogue */
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
searchGames: (payload: CatalogueSearchPayload, take: number, skip: number) =>
ipcRenderer.invoke("searchGames", payload, take, skip),
getCatalogue: (category: CatalogueCategory) =>
ipcRenderer.invoke("getCatalogue", category),
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
@ -44,10 +64,6 @@ contextBridge.exposeInMainWorld("electron", {
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
getHowLongToBeat: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getHowLongToBeat", objectId, shop),
getGames: (take?: number, skip?: number) =>
ipcRenderer.invoke("getGames", take, skip),
searchGameRepacks: (query: string) =>
ipcRenderer.invoke("searchGameRepacks", query),
getGameStats: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getGameStats", objectId, shop),
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
@ -78,9 +94,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
/* Download sources */
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
deleteDownloadSource: (id: number) =>
ipcRenderer.invoke("deleteDownloadSource", id),
putDownloadSource: (objectIds: string[]) =>
ipcRenderer.invoke("putDownloadSource", objectIds),
/* Library */
addGameToLibrary: (objectId: string, title: string, shop: GameShop) =>
@ -89,6 +104,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("createGameShortcut", id),
updateExecutablePath: (id: number, executablePath: string | null) =>
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
updateLaunchOptions: (id: number, launchOptions: string | null) =>
ipcRenderer.invoke("updateLaunchOptions", id, launchOptions),
selectGameWinePrefix: (id: number, winePrefixPath: string | null) =>
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
verifyExecutablePathInUse: (executablePath: string) =>
@ -100,8 +117,11 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("openGameInstallerPath", gameId),
openGameExecutablePath: (gameId: number) =>
ipcRenderer.invoke("openGameExecutablePath", gameId),
openGame: (gameId: number, executablePath: string) =>
ipcRenderer.invoke("openGame", gameId, executablePath),
openGame: (
gameId: number,
executablePath: string,
launchOptions: string | null
) => ipcRenderer.invoke("openGame", gameId, executablePath, launchOptions),
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
removeGameFromLibrary: (gameId: number) =>
ipcRenderer.invoke("removeGameFromLibrary", gameId),
@ -110,6 +130,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("deleteGameFolder", gameId),
getGameByObjectId: (objectId: string) =>
ipcRenderer.invoke("getGameByObjectId", objectId),
resetGameAchievements: (gameId: number) =>
ipcRenderer.invoke("resetGameAchievements", gameId),
onGamesRunning: (
cb: (
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
@ -130,6 +152,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Hardware */
getDiskFreeSpace: (path: string) =>
ipcRenderer.invoke("getDiskFreeSpace", path),
checkFolderWritePermission: (path: string) =>
ipcRenderer.invoke("checkFolderWritePermission", path),
/* Cloud save */
uploadSaveGame: (
@ -206,6 +230,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showOpenDialog", options),
showItemInFolder: (path: string) =>
ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
platform: process.platform,
/* Auto update */

View file

@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
@ -7,14 +7,13 @@ import {
useAppSelector,
useDownload,
useLibrary,
useRepacks,
useToast,
useUserDetails,
} from "@renderer/hooks";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
setSearch,
clearSearch,
setUserPreferences,
toggleDraggingDisabled,
closeToast,
@ -25,8 +24,9 @@ import {
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
import { downloadSourcesWorker } from "./workers";
import { repacksContext } from "./context";
import { logger } from "./logger";
import { downloadSourcesTable } from "./dexie";
import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import "./app.scss";
@ -40,35 +40,31 @@ export function App() {
const { t } = useTranslation("app");
const downloadSourceMigrationLock = useRef(false);
const { updateRepacks } = useRepacks();
const { clearDownload, setLastPacket } = useDownload();
const { indexRepacks } = useContext(repacksContext);
const {
userDetails,
hasActiveSubscription,
isFriendsModalVisible,
friendRequetsModalTab,
friendModalUserId,
syncFriendRequests,
hideFriendsModal,
} = useUserDetails();
const {
userDetails,
hasActiveSubscription,
fetchUserDetails,
updateUserDetails,
clearUserDetails,
} = useUserDetails();
const { hideHydraCloudModal, isHydraCloudModalVisible, hydraCloudFeature } =
useSubscription();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const location = useLocation();
const search = useAppSelector((state) => state.search.value);
const draggingDisabled = useAppSelector(
(state) => state.window.draggingDisabled
);
@ -103,6 +99,14 @@ export function App() {
};
}, [clearDownload, setLastPacket, updateLibrary]);
useEffect(() => {
const unsubscribe = window.electron.onHardDelete(() => {
updateLibrary();
});
return () => unsubscribe();
}, [updateLibrary]);
useEffect(() => {
const cachedUserDetails = window.localStorage.getItem("userDetails");
@ -126,7 +130,7 @@ export function App() {
const $script = document.createElement("script");
$script.id = "external-resources";
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}?t=${Date.now()}`;
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`;
document.head.appendChild($script);
});
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
@ -187,31 +191,6 @@ export function App() {
};
}, [onSignIn, updateLibrary, clearUserDetails]);
const handleSearch = useCallback(
(query: string) => {
dispatch(setSearch(query));
if (query === "") {
navigate(-1);
return;
}
const searchParams = new URLSearchParams({
query,
});
navigate(`/search?${searchParams.toString()}`, {
replace: location.pathname.startsWith("/search"),
});
},
[dispatch, location.pathname, navigate]
);
const handleClear = useCallback(() => {
dispatch(clearSearch());
navigate(-1);
}, [dispatch, navigate]);
useEffect(() => {
if (contentRef.current) contentRef.current.scrollTop = 0;
}, [location.pathname, location.search]);
@ -228,53 +207,31 @@ export function App() {
}, [dispatch, draggingDisabled]);
useEffect(() => {
if (downloadSourceMigrationLock.current) return;
updateRepacks();
downloadSourceMigrationLock.current = true;
const id = crypto.randomUUID();
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
window.electron.getDownloadSources().then(async (downloadSources) => {
if (!downloadSources.length) {
const id = crypto.randomUUID();
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
channel.onmessage = (event: MessageEvent<number>) => {
const newRepacksCount = event.data;
window.electron.publishNewRepacksNotification(newRepacksCount);
updateRepacks();
channel.onmessage = (event: MessageEvent<number>) => {
const newRepacksCount = event.data;
window.electron.publishNewRepacksNotification(newRepacksCount);
};
downloadSourcesTable.toArray().then((downloadSources) => {
downloadSources
.filter((source) => !source.fingerprint)
.forEach((downloadSource) => {
window.electron
.putDownloadSource(downloadSource.objectIds)
.then(({ fingerprint }) => {
downloadSourcesTable.update(downloadSource.id, { fingerprint });
});
});
});
};
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
}
for (const downloadSource of downloadSources) {
logger.info("Migrating download source", downloadSource.url);
const channel = new BroadcastChannel(
`download_sources:import:${downloadSource.url}`
);
await new Promise((resolve) => {
downloadSourcesWorker.postMessage([
"IMPORT_DOWNLOAD_SOURCE",
downloadSource.url,
]);
channel.onmessage = () => {
window.electron.deleteDownloadSource(downloadSource.id).then(() => {
resolve(true);
logger.info(
"Deleted download source from SQLite",
downloadSource.url
);
});
indexRepacks();
channel.close();
};
}).catch(() => channel.close());
}
downloadSourceMigrationLock.current = false;
});
}, [indexRepacks]);
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
}, [updateRepacks]);
const handleToastClose = useCallback(() => {
dispatch(closeToast());
@ -300,6 +257,12 @@ export function App() {
onClose={handleToastClose}
/>
<HydraCloudModal
visible={isHydraCloudModalVisible}
onClose={hideHydraCloudModal}
feature={hydraCloudFeature}
/>
{userDetails && (
<UserFriendModal
visible={isFriendsModalVisible}
@ -313,11 +276,7 @@ export function App() {
<Sidebar />
<article className="container">
<Header
onSearch={handleSearch}
search={search}
onClear={handleClear}
/>
<Header />
<section ref={contentRef} className="container__content">
<Outlet />

View file

@ -0,0 +1,13 @@
<svg width="55" height="49" viewBox="0 0 55 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.8501 29.1176L19.9196 28.3235L20.6957 25.6764L20.437 24.8823L18.1088 25.9411L14.487 24.3528L10.0891 23.0293L5.69128 23.5587L3.10431 25.6764L2.58691 29.1176L4.1391 33.6177L5.69128 36.7942L8.53695 38.9118L10.8652 38.3824L13.9696 34.9412V31.5L12.9348 29.1176V32.2941L10.8652 34.9412H7.50216L4.91519 32.2941L5.69128 28.3235L9.57174 26.4705L13.9696 27.5294L17.8501 29.1176Z" fill="white"/>
<path d="M36.9585 29.1176L34.889 28.3235L34.1129 25.6764L34.3716 24.8823L36.6998 25.9411L40.3216 24.3528L44.7195 23.0293L49.1173 23.5587L51.7043 25.6764L52.2217 29.1176L50.6695 33.6177L49.1173 36.7942L46.2716 38.9118L43.9434 38.3824L40.839 34.9412V31.5L41.8738 29.1176V32.2941L43.9434 34.9412H47.3064L49.8934 32.2941L49.1173 28.3235L45.2369 26.4705L40.839 27.5294L36.9585 29.1176Z" fill="white"/>
<path d="M40.3873 19.4005L38.8784 19.9071L38.7685 19.0593L38.5553 17.7049L39.811 14.777L41.6564 11.5721L44.483 9.44001L47.1023 9.23162L49.2244 10.9371L50.7089 14.4032L51.4925 17.1031L50.9665 19.9071L49.3381 20.8916L45.7182 20.6206L43.8957 18.6282L43.2332 16.675L44.9154 18.5142L47.5156 18.8992L49.4627 17.0344L49.5586 14.0673L47.0064 12.1987L43.7784 13.2776L41.7929 16.3293L40.3873 19.4005Z" fill="white"/>
<path d="M14.0238 19.4005L15.5327 19.9071L15.6426 19.0593L15.8559 17.7049L14.6001 14.777L12.7548 11.5721L9.92812 9.44001L7.30879 9.23162L5.18676 10.9371L3.70221 14.4032L2.91861 17.1031L3.44468 19.9071L5.07308 20.8916L8.69292 20.6206L10.5154 18.6282L11.178 16.675L9.4957 18.5142L6.89555 18.8992L4.94841 17.0344L4.8525 14.0673L7.4047 12.1987L10.6327 13.2776L12.6182 16.3293L14.0238 19.4005Z" fill="white"/>
<path d="M19.9494 36.4343L22.554 34.3372L21.9876 34.0884L20.9528 31.9707L19.9494 32.5001L17.5898 34.0884L15.3904 37.377L14.744 40.4414L15.2615 43.0885L17.0724 45.4709L20.9528 47.3238L23.6932 46.5297L25.435 44.7653L26.0028 42.9192L25.6093 39.0508L25.0919 37.7913L23.6932 37.0002L24.3158 39.105L24.3158 42.0296L22.3597 43.9181L19.9494 43.9181L18.0225 42.0296L17.4949 39.105L19.9494 36.4343Z" fill="white"/>
<path d="M35.0955 36.4343L32.4909 34.3372L33.0573 34.0884L34.0921 31.9707L35.0955 32.5001L37.4552 34.0884L39.6545 37.377L40.3009 40.4414L39.7834 43.0885L37.9725 45.4709L34.0921 47.3238L31.3518 46.5297L29.6099 44.7653L29.0421 42.9192L29.4356 39.0508L29.953 37.7913L31.3518 37.0002L30.7291 39.105L30.7291 42.0296L32.6852 43.9181L35.0955 43.9181L37.0224 42.0296L37.55 39.105L35.0955 36.4343Z" fill="white"/>
<path d="M18.8447 8.70593V5.79413L20.1382 5H22.9839L27.3817 5.79413L31.7796 5H34.6252L35.9187 5.79413V8.70593L38.7644 11.353L37.2122 15.0589L37.9883 20.8825L35.9187 23.0002L33.3317 23.7943L32.8144 26.1767L33.3317 28.8238L32.8144 30.9415L31.7796 33.0591L30.2274 33.8533H27.3817H24.536L22.9839 33.0591L21.9491 30.9415L21.4317 28.8238L21.9491 26.1767L21.4317 23.7943L18.8447 23.0002L16.7751 20.8825L17.5512 15.0589L15.999 11.353L18.8447 8.70593Z" fill="white"/>
<path d="M15.5205 6.88232L16.2966 8.73528L17.5901 7.67645V4.76465L15.5205 6.88232Z" fill="white"/>
<path d="M39.2861 6.88232L38.51 8.73528L37.2166 7.67645V4.76465L39.2861 6.88232Z" fill="white"/>
<path d="M18.3667 2.11767L18.6254 4.23534L19.4015 3.70593H20.9537L20.4363 2.11767L17.0732 0L18.3667 2.11767Z" fill="white"/>
<path d="M35.6997 2.11767L35.441 4.23534L34.6649 3.70593H33.1127L33.6301 2.11767L36.9932 0L35.6997 2.11767Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -7,9 +7,5 @@ export interface BadgeProps {
}
export function Badge({ children }: BadgeProps) {
return (
<div className="badge">
<span>{children}</span>
</div>
);
return <div className="badge">{children}</div>;
}

View file

@ -15,7 +15,7 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
return (
<div className="checkbox-field">
<div className="checkbox-field__checkbox">
<div className={`checkbox-field__checkbox ${props.checked ? 'checked' : ''}`}>
<input
id={id}
type="checkbox"

View file

@ -0,0 +1,68 @@
@use "../../scss/globals.scss";
.dropdown-menu {
&__content {
background-color: globals.$dark-background-color;
border: 1px solid globals.$border-color;
border-radius: 6px;
min-width: 200px;
flex-direction: column;
align-items: center;
}
&__group {
width: 100%;
padding: 4px;
}
&__title-bar {
width: 100%;
padding: 4px 12px;
font-size: 14px;
font-weight: 500;
color: globals.$muted-color;
}
&__separator {
width: 100%;
height: 1px;
background-color: globals.$border-color;
}
&__item {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 8px;
border-radius: 4px;
padding: 5px 12px;
cursor: pointer;
transition: background-color 0.1s ease-in-out;
font-size: 14px;
}
&__item--disabled {
cursor: default;
opacity: 0.6;
}
&:not(&__item--disabled) &__item:hover {
background-color: globals.$background-color;
color: globals.$muted-color;
}
&__item:focus {
background-color: globals.$background-color;
outline: none;
}
&__item-icon {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
}

View file

@ -0,0 +1,81 @@
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import "./dropdown-menu.scss";
export interface DropdownMenuItem {
icon?: React.ReactNode;
label: string;
disabled?: boolean;
show?: boolean;
onClick?: () => void;
}
interface DropdownMenuProps {
children: React.ReactNode;
title?: string;
loop?: boolean;
items: DropdownMenuItem[];
sideOffset?: number;
side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
alignOffset?: number;
}
export function DropdownMenu({
children,
title,
items,
sideOffset = 5,
side = "bottom",
loop = true,
align = "center",
alignOffset = 0,
}: DropdownMenuProps) {
return (
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild>
<div aria-label={title}>{children}</div>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
sideOffset={sideOffset}
side={side}
loop={loop}
align={align}
alignOffset={alignOffset}
className="dropdown-menu__content"
>
{title && (
<DropdownMenuPrimitive.Group className="dropdown-menu__group">
<div className="dropdown-menu__title-bar">{title}</div>
</DropdownMenuPrimitive.Group>
)}
<DropdownMenuPrimitive.Separator className="dropdown-menu__separator" />
<DropdownMenuPrimitive.Group className="dropdown-menu__group">
{items.map(
(item) =>
item.show !== false && (
<DropdownMenuPrimitive.Item
key={item.label}
aria-label={item.label}
onSelect={item.onClick}
className={`dropdown-menu__item ${item.disabled ? "dropdown-menu__item--disabled" : ""}`}
disabled={item.disabled}
>
{item.icon && (
<div className="dropdown-menu__item-icon">
{item.icon}
</div>
)}
{item.label}
</DropdownMenuPrimitive.Item>
)
)}
</DropdownMenuPrimitive.Group>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
);
}

View file

@ -1,5 +1,5 @@
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import type { CatalogueEntry, GameRepack, GameStats } from "@types";
import type { GameStats } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
@ -7,16 +7,16 @@ import "./game-card.scss";
import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge";
import { useCallback, useContext, useEffect, useState } from "react";
import { useFormat } from "@renderer/hooks";
import { repacksContext } from "@renderer/context";
import { useCallback, useState } from "react";
import { useFormat, useRepacks } from "@renderer/hooks";
import { steamUrlBuilder } from "@shared";
export interface GameCardProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
game: CatalogueEntry;
game: any;
}
const shopIcon = {
@ -27,20 +27,12 @@ export function GameCard({ game, ...props }: GameCardProps) {
const { t } = useTranslation("game_card");
const [stats, setStats] = useState<GameStats | null>(null);
const [repacks, setRepacks] = useState<GameRepack[]>([]);
const { searchRepacks, isIndexingRepacks } = useContext(repacksContext);
useEffect(() => {
if (!isIndexingRepacks) {
searchRepacks(game.title).then((repacks) => {
setRepacks(repacks);
});
}
}, [game, isIndexingRepacks, searchRepacks]);
const { getRepacksForObjectId } = useRepacks();
const repacks = getRepacksForObjectId(game.objectId);
const uniqueRepackers = Array.from(
new Set(repacks.map(({ repacker }) => repacker))
new Set(repacks.map((repack) => repack.repacker))
);
const handleHover = useCallback(() => {
@ -62,7 +54,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
>
<div className="game-card__backdrop">
<img
src={game.cover}
src={steamUrlBuilder.library(game.objectId)}
alt={game.title}
className="game-card__cover"
loading="lazy"

View file

@ -6,17 +6,10 @@ import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
import { useAppDispatch, useAppSelector } from "@renderer/hooks";
import "./header.scss";
import { clearSearch } from "@renderer/features";
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
import { setFilters } from "@renderer/features";
import cn from "classnames";
export interface HeaderProps {
onSearch: (query: string) => void;
onClear: () => void;
search?: string;
}
const pathTitle: Record<string, string> = {
"/": "home",
"/catalogue": "catalogue",
@ -24,7 +17,7 @@ const pathTitle: Record<string, string> = {
"/settings": "settings",
};
export function Header({ onSearch, onClear, search }: HeaderProps) {
export function Header() {
const inputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
@ -33,6 +26,11 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
const { headerTitle, draggingDisabled } = useAppSelector(
(state) => state.window
);
const searchValue = useAppSelector(
(state) => state.catalogueSearch.filters.title
);
const dispatch = useAppDispatch();
const [isFocused, setIsFocused] = useState(false);
@ -48,12 +46,6 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
return t(pathTitle[location.pathname]);
}, [location.pathname, headerTitle, t]);
useEffect(() => {
if (search && !location.pathname.startsWith("/search")) {
dispatch(clearSearch());
}
}, [location.pathname, search, dispatch]);
const focusInput = () => {
setIsFocused(true);
inputRef.current?.focus();
@ -67,6 +59,20 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
navigate(-1);
};
const handleSearch = (value: string) => {
dispatch(setFilters({ title: value }));
if (!location.pathname.startsWith("/catalogue")) {
navigate("/catalogue");
}
};
useEffect(() => {
if (!location.pathname.startsWith("/catalogue") && searchValue) {
dispatch(setFilters({ title: "" }));
}
}, [location.pathname, searchValue, dispatch]);
return (
<>
<header
@ -115,17 +121,17 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
type="text"
name="search"
placeholder={t("search")}
value={search}
value={searchValue}
className="header__search-input"
onChange={(event) => onSearch(event.target.value)}
onChange={(event) => handleSearch(event.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
/>
{search && (
{searchValue && (
<button
type="button"
onClick={onClear}
onClick={() => dispatch(setFilters({ title: "" }))}
className="header__action-button"
>
<XIcon />

Some files were not shown because too many files have changed in this diff Show more