mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge branch 'main' into lang-ca
This commit is contained in:
commit
7aa02f9d64
103 changed files with 2756 additions and 740 deletions
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "مميّز",
|
||||
"recently_added": "مضاف مؤخراً",
|
||||
"trending": "شائع",
|
||||
"surprise_me": "فاجئني",
|
||||
"no_results": "لم يتم العثور على نتائج"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (متوقف)",
|
||||
"downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)",
|
||||
"filter": "بحث في المكتبة",
|
||||
"follow_us": "تابعنا",
|
||||
"home": "الرئيسية",
|
||||
"discord": "انضم إلى الـDiscord الخاص بنا",
|
||||
"telegram": "انضم إلى قناة Telegram الخاصة بنا",
|
||||
"x": "تابعنا على X",
|
||||
"github": "ساهم في مشروعنا على GitHub"
|
||||
"home": "الرئيسية"
|
||||
},
|
||||
"header": {
|
||||
"search": "ابحث عن الألعاب",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "إيقاف",
|
||||
"cancel": "إلغاء",
|
||||
"remove": "إزالة",
|
||||
"remove_from_list": "إزالة",
|
||||
"space_left_on_disk": "{{space}} متبقية على القرص",
|
||||
"eta": "الوقت المتبقي {{eta}}",
|
||||
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "متطلبات النظام",
|
||||
"minimum": "الحد الأدنى",
|
||||
"recommended": "موصى به",
|
||||
"no_minimum_requirements": "{{title}} لا تتوفر معلومات عن الحد الأدنى للمتطلبات",
|
||||
"no_recommended_requirements": "{{title}} لا تتوفر معلومات عن المتطلبات الموصى بها",
|
||||
"release_date": "تم الإصدار في {{date}}",
|
||||
"publisher": "نشر بواسطة {{publisher}}",
|
||||
"copy_link_to_clipboard": "نسخ الرابط",
|
||||
"copied_link_to_clipboard": "تم نسخ الرابط",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
|
@ -84,14 +73,6 @@
|
|||
"repacks_modal_description": "اختر الحزمة التي تريد تنزيلها",
|
||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى الإعدادات",
|
||||
"download_now": "تنزيل الآن",
|
||||
"installation_instructions": "إرشادات التثبيت",
|
||||
"installation_instructions_description": "هناك خطوات إضافية مطلوبة لتثبيت هذه اللعبة",
|
||||
"online_fix_instruction": "تتطلب ألعاب OnlineFix كلمة مرور لاستخراجها. عند الحاجة، استخدم كلمة المرور التالية:",
|
||||
"dodi_installation_instruction": "عند فتح مثبت DODI، اضغط على مفتاح التشغيل لأعلى <0 /> لبدء عملية التثبيت:",
|
||||
"dont_show_it_again": "لا تعرضها مرة أخرى",
|
||||
"copy_to_clipboard": "نسخ",
|
||||
"copied_to_clipboard": "تم النسخ",
|
||||
"got_it": "حسنأ",
|
||||
"no_shop_details": "لم يتم استرداد تفاصيل المتجر.",
|
||||
"download_options": "خيارات التنزيل",
|
||||
"download_path": "مسار التنزيل",
|
||||
|
@ -114,17 +95,13 @@
|
|||
"eta": "الوقت المتبقي {{eta}}",
|
||||
"paused": "متوقفة مؤقتًا",
|
||||
"verifying": "جار التحقق…",
|
||||
"completed_at": "اكتمل في {{date}}",
|
||||
"completed": "اكتمل",
|
||||
"download_again": "تحميل مرة أخرى",
|
||||
"cancel": "إلغاء",
|
||||
"filter": "تصفية الألعاب التي تم تنزيلها",
|
||||
"remove": "إزالة",
|
||||
"downloading_metadata": "جار تنزيل البيانات الوصفية…",
|
||||
"starting_download": "يبدأ التنزيل…",
|
||||
"deleting": "جار حذف المثبت…",
|
||||
"delete": "إزالة المثبت",
|
||||
"remove_from_list": "إزالة",
|
||||
"delete_modal_title": "هل أنت متأكد؟",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
|
||||
"install": "تثبيت"
|
||||
|
@ -135,8 +112,6 @@
|
|||
"notifications": "الإشعارات",
|
||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة جديدة",
|
||||
"telemetry": "القياس عن بعد",
|
||||
"telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر",
|
||||
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal-Debrid ",
|
||||
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
|
||||
"launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "Рэкамэндаванае",
|
||||
"recently_added": "Нядаўна дададзенае",
|
||||
"trending": "Актуальнае",
|
||||
"surprise_me": "Здзіві мяне",
|
||||
"no_results": "Няма вынікаў"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (Спынена)",
|
||||
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
||||
"filter": "Фільтар бібліятэкі",
|
||||
"follow_us": "Падпісвайцеся на нас",
|
||||
"home": "Галоўная",
|
||||
"discord": "Далучайцеся да Discord",
|
||||
"telegram": "Далучайцеся да Telegram",
|
||||
"x": "Падпісвайцеся на X",
|
||||
"github": "Зрабіць свой унёсак на GitHub"
|
||||
"home": "Галоўная"
|
||||
},
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "Спыніць",
|
||||
"cancel": "Скасаваць",
|
||||
"remove": "Выдаліць",
|
||||
"remove_from_list": "Выдаліць",
|
||||
"space_left_on_disk": "{{space}} засталося на дыску",
|
||||
"eta": "Канчатак {{eta}}",
|
||||
"downloading_metadata": "Сцягванне мэтаданых…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "Сістэмныя патрэбаванни",
|
||||
"minimum": "Мінімальныя",
|
||||
"recommended": "Рэкамендуемыя",
|
||||
"no_minimum_requirements": "{{title}} ня ўтрымлівае інфармацыі пра мінімальныя патрабаванні",
|
||||
"no_recommended_requirements": "{{title}} ня ўтрымлівае інфармацыі пра рэкамендуемыя патрабаванні",
|
||||
"release_date": "Выпушчана {{date}}",
|
||||
"publisher": "Выдана {{publisher}}",
|
||||
"copy_link_to_clipboard": "Скапіяваць спасылку",
|
||||
"copied_link_to_clipboard": "Спасылка скапіявана",
|
||||
"hours": "гадзін",
|
||||
"minutes": "хвілін",
|
||||
"amount_hours": "{{amount}} гадзін",
|
||||
|
@ -83,15 +72,7 @@
|
|||
"change": "Змяніць",
|
||||
"repacks_modal_description": "Абярыце рэпак, які хочаце сцягнуць",
|
||||
"select_folder_hint": "Каб змяніць папку па змоўчанні, адкрыйце",
|
||||
"download_now": "Сцягнуць зараз",
|
||||
"installation_instructions": "Інструкцыя ўсталёўкі",
|
||||
"installation_instructions_description": "Усталёўка гэтай гульні патрабуе дадатковых крокаў",
|
||||
"online_fix_instruction": "Гульні з OnlineFix патрабуюць пароль для вымання. Калі неабходна, выкарыстоўвайце наступны пароль:",
|
||||
"dodi_installation_instruction": "Калі вы адкрыеце ўсталёўшчык DODI, націсніце на клявіятуры клявішу 'уверх' <0 />, каб пачаць працэс усталёўкі:",
|
||||
"dont_show_it_again": "Не паказваць зноў",
|
||||
"copy_to_clipboard": "Капіяваць",
|
||||
"copied_to_clipboard": "Скапіявана",
|
||||
"got_it": "Зразумела"
|
||||
"download_now": "Сцягнуць зараз"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Актываваць Hydra",
|
||||
|
@ -107,17 +88,13 @@
|
|||
"eta": "Канчатак {{eta}}",
|
||||
"paused": "Спынена",
|
||||
"verifying": "Праверка…",
|
||||
"completed_at": "Скончана а {{date}}",
|
||||
"completed": "Скончана",
|
||||
"download_again": "Сцягнуць зноў",
|
||||
"cancel": "Скасаваць",
|
||||
"filter": "Фільтар сцягнутых гульняў",
|
||||
"remove": "Выдаліць",
|
||||
"downloading_metadata": "Сцягванне мэтаданых…",
|
||||
"starting_download": "Пачатак сцягвання…",
|
||||
"deleting": "Выдаленне ўсталёўшчыка…",
|
||||
"delete": "Выдаліць усталёўшчык",
|
||||
"remove_from_list": "Выдаліць",
|
||||
"delete_modal_title": "Вы ўпэўнены?",
|
||||
"delete_modal_description": "Гэта выдаліць усе файлы ўсталёвак з вашага кампутара",
|
||||
"install": "Усталяваць"
|
||||
|
@ -128,8 +105,6 @@
|
|||
"notifications": "Апавяшчэнні",
|
||||
"enable_download_notifications": "Па сканчэнні сцягванні",
|
||||
"enable_repack_list_notifications": "Пры даданні новага рэпака",
|
||||
"telemetry": "Тэлеметрыя",
|
||||
"telemetry_description": "Уключыць ананімную статыстыку выкарыстання",
|
||||
"behavior": "Паводзіны",
|
||||
"quit_app_instead_hiding": "Закрываць праграму замест таго, каб хаваць яе ў трэй",
|
||||
"launch_with_system": "Запускаць праграму пры запуску сыстэмы"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "Anbefalet",
|
||||
"recently_added": "Nyligt tilføjet",
|
||||
"trending": "Trender",
|
||||
"surprise_me": "Overrask mig",
|
||||
"no_results": "Ingen resultater fundet"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (Paused)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filtrer bibliotek",
|
||||
"follow_us": "Følg os",
|
||||
"home": "Hjem",
|
||||
"discord": "Tilslut dig vores Discord",
|
||||
"telegram": "Tilslut dig vores Telegram",
|
||||
"x": "Følg på X",
|
||||
"github": "Bidrag på GitHub"
|
||||
"home": "Hjem"
|
||||
},
|
||||
"header": {
|
||||
"search": "Søg spil",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "Pause",
|
||||
"cancel": "Annullér",
|
||||
"remove": "Fjern",
|
||||
"remove_from_list": "Fjern",
|
||||
"space_left_on_disk": "{{space}} tilbage på harddisken",
|
||||
"eta": "Konklusion {{eta}}",
|
||||
"downloading_metadata": "Downloader metadata…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "System behov",
|
||||
"minimum": "Mindste",
|
||||
"recommended": "Anbefalet",
|
||||
"no_minimum_requirements": "{{title}} angiver ikke mindste behov informationer",
|
||||
"no_recommended_requirements": "{{title}} angiver ikke anbefalet behov informationer",
|
||||
"release_date": "Offentliggjort den {{date}}",
|
||||
"publisher": "Udgivet af {{publisher}}",
|
||||
"copy_link_to_clipboard": "Kopier link",
|
||||
"copied_link_to_clipboard": "Link kopieret",
|
||||
"hours": "timer",
|
||||
"minutes": "minutter",
|
||||
"amount_hours": "{{amount}} timer",
|
||||
|
@ -83,15 +72,7 @@
|
|||
"change": "Ændré",
|
||||
"repacks_modal_description": "Vælg den repack du vil downloade",
|
||||
"select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne</0>",
|
||||
"download_now": "Download nu",
|
||||
"installation_instructions": "Installations Instrukser",
|
||||
"installation_instructions_description": "Yderligere skridt er krævet for at installere dette spil",
|
||||
"online_fix_instruction": "OnlineFix spil kræver et kodeord for at kunne blive udpakket. Når krævet, brug det følgende kodeord:",
|
||||
"dodi_installation_instruction": "Når du åbner DODI installatør, tryk på op-knappen på dit tastatur <0 /> for at starte installations processen:",
|
||||
"dont_show_it_again": "Vis ikke igen",
|
||||
"copy_to_clipboard": "Kopier",
|
||||
"copied_to_clipboard": "Kopieret",
|
||||
"got_it": "Forstået"
|
||||
"download_now": "Download nu"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Aktivér Hydra",
|
||||
|
@ -107,17 +88,13 @@
|
|||
"eta": "Konklusion {{eta}}",
|
||||
"paused": "Pauset",
|
||||
"verifying": "Verificerer…",
|
||||
"completed_at": "Færdiggjort på {{date}}",
|
||||
"completed": "Færdigt",
|
||||
"download_again": "Download igen",
|
||||
"cancel": "Annullér",
|
||||
"filter": "Filtrer downloadet spil",
|
||||
"remove": "Fjern",
|
||||
"downloading_metadata": "Downloader metadata…",
|
||||
"starting_download": "Starter download…",
|
||||
"deleting": "Sletter installatør…",
|
||||
"delete": "Fjern installatør",
|
||||
"remove_from_list": "Fjern",
|
||||
"delete_modal_title": "Er du sikker?",
|
||||
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
|
||||
"install": "Installér"
|
||||
|
@ -128,8 +105,6 @@
|
|||
"notifications": "Notifikationer",
|
||||
"enable_download_notifications": "Når et download bliver færdigt",
|
||||
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
|
||||
"telemetry": "Telemetri",
|
||||
"telemetry_description": "Slå anonymt brugs statistik til",
|
||||
"quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen",
|
||||
"launch_with_system": "Åben Hydra ved start af systemet",
|
||||
"general": "Generelt",
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"app": {
|
||||
"successfully_signed_in": "Successfully signed in"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Featured",
|
||||
"trending": "Trending",
|
||||
|
@ -16,7 +19,8 @@
|
|||
"filter": "Filter library",
|
||||
"home": "Home",
|
||||
"queued": "{{title}} (Queued)",
|
||||
"game_has_no_executable": "Game has no executable selected"
|
||||
"game_has_no_executable": "Game has no executable selected",
|
||||
"sign_in": "Sign in"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
|
@ -49,7 +53,6 @@
|
|||
"pause": "Pause",
|
||||
"cancel": "Cancel",
|
||||
"remove": "Remove",
|
||||
"remove_from_list": "Remove",
|
||||
"space_left_on_disk": "{{space}} left on disk",
|
||||
"eta": "Conclusion {{eta}}",
|
||||
"calculating_eta": "Calculating remaining time…",
|
||||
|
@ -58,13 +61,9 @@
|
|||
"requirements": "System requirements",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Recommended",
|
||||
"no_minimum_requirements": "{{title}} doesn't provide minimum requirements information",
|
||||
"no_recommended_requirements": "{{title}} doesn't provide recommended requirements information",
|
||||
"paused": "Paused",
|
||||
"release_date": "Released on {{date}}",
|
||||
"publisher": "Published by {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copy link",
|
||||
"copied_link_to_clipboard": "Link copied",
|
||||
"hours": "hours",
|
||||
"minutes": "minutes",
|
||||
"amount_hours": "{{amount}} hours",
|
||||
|
@ -85,14 +84,6 @@
|
|||
"repacks_modal_description": "Choose the repack you want to download",
|
||||
"select_folder_hint": "To change the default folder, go to the <0>Settings</0>",
|
||||
"download_now": "Download now",
|
||||
"installation_instructions": "Installation Instructions",
|
||||
"installation_instructions_description": "Additional steps are required to install this game",
|
||||
"online_fix_instruction": "OnlineFix games requires a password to be extracted. When required, use the following password:",
|
||||
"dodi_installation_instruction": "When you open DODI installer, press your keyboard up key <0 /> to start the installation process:",
|
||||
"dont_show_it_again": "Don't show it again",
|
||||
"copy_to_clipboard": "Copy",
|
||||
"copied_to_clipboard": "Copied",
|
||||
"got_it": "Got it",
|
||||
"no_shop_details": "Could not retrieve shop details.",
|
||||
"download_options": "Download options",
|
||||
"download_path": "Download path",
|
||||
|
@ -119,7 +110,9 @@
|
|||
"danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra",
|
||||
"download_in_progress": "Download in progress",
|
||||
"download_paused": "Download paused",
|
||||
"last_downloaded_option": "Last downloaded option"
|
||||
"last_downloaded_option": "Last downloaded option",
|
||||
"create_shortcut_success": "Shortcut created successfully",
|
||||
"create_shortcut_error": "Error creating shortcut"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
|
@ -135,18 +128,14 @@
|
|||
"eta": "Conclusion {{eta}}",
|
||||
"paused": "Paused",
|
||||
"verifying": "Verifying…",
|
||||
"completed_at": "Completed in {{date}}",
|
||||
"completed": "Completed",
|
||||
"removed": "Not downloaded",
|
||||
"download_again": "Download again",
|
||||
"cancel": "Cancel",
|
||||
"filter": "Filter downloaded games",
|
||||
"remove": "Remove",
|
||||
"downloading_metadata": "Downloading metadata…",
|
||||
"starting_download": "Starting download…",
|
||||
"deleting": "Deleting installer…",
|
||||
"delete": "Remove installer",
|
||||
"remove_from_list": "Remove",
|
||||
"delete_modal_title": "Are you sure?",
|
||||
"delete_modal_description": "This will remove all the installation files from your computer",
|
||||
"install": "Install",
|
||||
|
@ -163,8 +152,6 @@
|
|||
"notifications": "Notifications",
|
||||
"enable_download_notifications": "When a download is complete",
|
||||
"enable_repack_list_notifications": "When a new repack is added",
|
||||
"telemetry": "Telemetry",
|
||||
"telemetry_description": "Enable anonymous usage statistics",
|
||||
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||
"quit_app_instead_hiding": "Don't hide Hydra when closing",
|
||||
"launch_with_system": "Launch Hydra on system start-up",
|
||||
|
@ -198,7 +185,12 @@
|
|||
"sync_download_sources": "Sync sources",
|
||||
"removed_download_source": "Download source removed",
|
||||
"added_download_source": "Added download source",
|
||||
"download_sources_synced": "All download sources are synced"
|
||||
"download_sources_synced": "All download sources are synced",
|
||||
"insert_valid_json_url": "Insert a valid JSON url",
|
||||
"found_download_option_zero": "No download option found",
|
||||
"found_download_option_one": "Found {{countFormatted}} download option",
|
||||
"found_download_option_other": "Found {{countFormatted}} download options",
|
||||
"import": "Import"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
@ -224,5 +216,27 @@
|
|||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Toggle password visibility"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} hours",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"last_time_played": "Last played {{period}}",
|
||||
"activity": "Recent activity",
|
||||
"library": "Library",
|
||||
"total_play_time": "Total playtime: {{amount}}",
|
||||
"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",
|
||||
"saving": "Saving",
|
||||
"save": "Save",
|
||||
"edit_profile": "Edit Profile",
|
||||
"saved_successfully": "Saved successfully",
|
||||
"try_again": "Please, try again",
|
||||
"sign_out_modal_title": "Are you sure?",
|
||||
"cancel": "Cancel",
|
||||
"successfully_signed_out": "Successfully signed out",
|
||||
"sign_out": "Sign out",
|
||||
"playing_for": "Playing for {{amount}}",
|
||||
"sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"app": {
|
||||
"successfully_signed_in": "Sesión iniciada correctamente"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destacado",
|
||||
"trending": "Tendencias",
|
||||
|
@ -16,7 +19,8 @@
|
|||
"filter": "Buscar en la biblioteca",
|
||||
"home": "Inicio",
|
||||
"queued": "{{title}} (En Cola)",
|
||||
"game_has_no_executable": "El juego no tiene un ejecutable"
|
||||
"game_has_no_executable": "El juego no tiene un ejecutable",
|
||||
"sign_in": "Iniciar sesión"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar juegos",
|
||||
|
@ -49,7 +53,6 @@
|
|||
"pause": "Pausa",
|
||||
"cancel": "Cancelar",
|
||||
"remove": "Eliminar",
|
||||
"remove_from_list": "Quitar",
|
||||
"space_left_on_disk": "{{space}} restantes en el disco",
|
||||
"eta": "Tiempo restante: {{eta}}",
|
||||
"calculating_eta": "Calculando tiempo restante…",
|
||||
|
@ -58,13 +61,9 @@
|
|||
"requirements": "Requisitos del Sistema",
|
||||
"minimum": "Mínimos",
|
||||
"recommended": "Recomendados",
|
||||
"no_minimum_requirements": "Sin requisitos mínimos para {{title}}",
|
||||
"no_recommended_requirements": "{{title}} no tiene requisitos recomendados",
|
||||
"paused": "Pausado",
|
||||
"release_date": "Fecha de lanzamiento: {{date}}",
|
||||
"publisher": "Publicado por: {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copiar enlace",
|
||||
"copied_link_to_clipboard": "Enlace copiado",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos",
|
||||
"amount_hours": "{{amount}} horas",
|
||||
|
@ -85,14 +84,6 @@
|
|||
"repacks_modal_description": "Selecciona el repack que quieres descargar",
|
||||
"select_folder_hint": "Para cambiar la carpeta predeterminada, ve a <0>Ajustes</0>",
|
||||
"download_now": "Descargar ahora",
|
||||
"installation_instructions": "Instrucciones de instalación",
|
||||
"installation_instructions_description": "Se requieren de pasos adicionales para instalar este juego",
|
||||
"online_fix_instruction": "Los juegos de OnlineFix requieren una contraseña para ser extraídos. Cuando se requiera, usa la siguiente contraseña:",
|
||||
"dodi_installation_instruction": "Cuando abras el instalador de DODI, presiona la tecla hacia arriba del teclado <0 /> para iniciar el proceso de instalación:",
|
||||
"dont_show_it_again": "No mostrar de nuevo",
|
||||
"copy_to_clipboard": "Copiar",
|
||||
"copied_to_clipboard": "Copiado",
|
||||
"got_it": "Entendido",
|
||||
"no_shop_details": "No se pudieron obtener detalles de la tienda.",
|
||||
"download_options": "Opciones de descarga",
|
||||
"download_path": "Ruta de descarga",
|
||||
|
@ -119,7 +110,9 @@
|
|||
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra",
|
||||
"download_in_progress": "Descarga en progreso",
|
||||
"download_paused": "Descarga pausada",
|
||||
"last_downloaded_option": "Última opción descargada"
|
||||
"last_downloaded_option": "Última opción descargada",
|
||||
"create_shortcut_success": "Atajo creado con éxito",
|
||||
"create_shortcut_error": "Error al crear un atajo"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activar Hydra",
|
||||
|
@ -135,18 +128,14 @@
|
|||
"eta": "Finalizando en {{eta}}",
|
||||
"paused": "En Pausa",
|
||||
"verifying": "Verificando…",
|
||||
"completed_at": "Completado el {{date}}",
|
||||
"completed": "Completado",
|
||||
"removed": "No descargado",
|
||||
"download_again": "Descargar de nuevo",
|
||||
"cancel": "Cancelar",
|
||||
"filter": "Buscar juegos descargados",
|
||||
"remove": "Eliminar",
|
||||
"downloading_metadata": "Descargando metadatos…",
|
||||
"starting_download": "Iniciando descarga…",
|
||||
"deleting": "Eliminando instalador…",
|
||||
"delete": "Eliminar instalador",
|
||||
"remove_from_list": "Eliminar",
|
||||
"delete_modal_title": "¿Estás seguro?",
|
||||
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
|
||||
"install": "Instalar",
|
||||
|
@ -163,8 +152,6 @@
|
|||
"notifications": "Notificaciones",
|
||||
"enable_download_notifications": "Cuando se completa una descarga",
|
||||
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
||||
"telemetry": "Telemetría",
|
||||
"telemetry_description": "Habilitar recopilación de datos de manera anónima",
|
||||
"real_debrid_api_token_label": "Token API de Real-Debrid",
|
||||
"quit_app_instead_hiding": "Salir de Hydra en vez de minimizar en la bandeja del sistema",
|
||||
"launch_with_system": "Iniciar Hydra al inicio del sistema",
|
||||
|
@ -198,7 +185,12 @@
|
|||
"sync_download_sources": "Sincronizar fuentes",
|
||||
"removed_download_source": "Fuente de descarga eliminada",
|
||||
"added_download_source": "Fuente de descarga añadida",
|
||||
"download_sources_synced": "Todas las fuentes de descarga estánn actualizadas"
|
||||
"download_sources_synced": "Todas las fuentes de descargas están actualizadas.",
|
||||
"insert_valid_json_url": "Introduce una URL JSON válida",
|
||||
"found_download_option_zero": "No se encontró una opción de descarga",
|
||||
"found_download_option_one": "Se encontró {{countFormatted}} opción de descarga",
|
||||
"found_download_option_other": "Se encontraron {{countFormatted}} opciones de descarga",
|
||||
"import": "Importar"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Descarga completada",
|
||||
|
@ -224,5 +216,27 @@
|
|||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Cambiar visibilidad de contraseña"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"last_time_played": "Última vez jugado {{period}}",
|
||||
"activity": "Actividad reciente",
|
||||
"library": "Biblioteca",
|
||||
"total_play_time": "Total de tiempo jugado: {{amount}}",
|
||||
"no_recent_activity_title": "Que raro, no hay nada por acá, ¿que tal si jugamos algo para empezar?",
|
||||
"no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!",
|
||||
"display_name": "Nombre a mostrar",
|
||||
"saving": "Guardando",
|
||||
"save": "Guardar",
|
||||
"edit_profile": "Editar perfil",
|
||||
"saved_successfully": "Guardado exitosamente",
|
||||
"try_again": "Por favor, intenta de nuevo",
|
||||
"sign_out_modal_title": "¿Estás seguro?",
|
||||
"cancel": "Cancelar",
|
||||
"successfully_signed_out": "Sesión cerrada exitosamente",
|
||||
"sign_out": "Cerrar sesión",
|
||||
"playing_for": "Jugando por {{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?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "پیشنهادی",
|
||||
"recently_added": "تازه اضافه شده",
|
||||
"trending": "پرطرفدار",
|
||||
"surprise_me": "سوپرایزم کن",
|
||||
"no_results": "اتمامای پیدا نشد"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (متوقف شده)",
|
||||
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
||||
"filter": "فیلتر کردن کتابخانه",
|
||||
"follow_us": "دنبال کردن ما",
|
||||
"home": "خانه",
|
||||
"discord": "عضویت در دیسکورد ما",
|
||||
"telegram": "عضویت در تلگرام ما",
|
||||
"x": "دنبال کرد در ایکس",
|
||||
"github": "مشارکت در گیتهاب"
|
||||
"home": "خانه"
|
||||
},
|
||||
"header": {
|
||||
"search": "جستجوی بازیها",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "توقف",
|
||||
"cancel": "بیخیال",
|
||||
"remove": "حذف",
|
||||
"remove_from_list": "حذف",
|
||||
"space_left_on_disk": "{{space}} فضا در دیسک باقیمانده",
|
||||
"eta": "اتمام {{eta}}",
|
||||
"downloading_metadata": "در حال دانلود متادیتاها…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "سیستم مورد نیاز",
|
||||
"minimum": "حداقل",
|
||||
"recommended": "پیشنهادی",
|
||||
"no_minimum_requirements": "{{title}} اطلاعات حداقل سیستم مورد نیاز را فراهم نکرده",
|
||||
"no_recommended_requirements": "{{title}} اطلاعات پیشنهادی سیستم مورد نیاز را فراهم نکرده",
|
||||
"release_date": "منتشر شده در {{date}}",
|
||||
"publisher": "منتشر شده توسط {{publisher}}",
|
||||
"copy_link_to_clipboard": "کپی لینک",
|
||||
"copied_link_to_clipboard": "لینک کپی شد",
|
||||
"hours": "ساعت",
|
||||
"minutes": "دقیقه",
|
||||
"amount_hours": "{{amount}} ساعت",
|
||||
|
@ -83,15 +72,7 @@
|
|||
"change": "تغییر",
|
||||
"repacks_modal_description": "ریپک مورد نظر برای دانلود را انتخاب کنید",
|
||||
"select_folder_hint": "برای تغییر پوشهی پیشفرض به <0>Settings</0> بروید",
|
||||
"download_now": "الان دانلود کن",
|
||||
"installation_instructions": "دستورات نصب",
|
||||
"installation_instructions_description": "قدمهای دیگری برای نصب این بازی نیاز است",
|
||||
"online_fix_instruction": "بازیهای OnlineFix برای اکسترکت شدن به پسوورد نیاز دارند. در صورت نیاز، از این پسوورد استفاده کنید:",
|
||||
"dodi_installation_instruction": "زمانی که اینستالر DODI را باز کردید، دکمهی <0 /> را فشار دهید تا فرایند نصب شروع شود:",
|
||||
"dont_show_it_again": "دیگر نمایش نده",
|
||||
"copy_to_clipboard": "کپی",
|
||||
"copied_to_clipboard": "کپی شد",
|
||||
"got_it": "فهمیدم"
|
||||
"download_now": "الان دانلود کن"
|
||||
},
|
||||
"activation": {
|
||||
"title": "فعال کردن هایدرا",
|
||||
|
@ -107,17 +88,13 @@
|
|||
"eta": "اتمام {{eta}}",
|
||||
"paused": "متوقف شده",
|
||||
"verifying": "در حال اعتبارسنجی…",
|
||||
"completed_at": "پایان یافته در {{date}}",
|
||||
"completed": "پایان یافته",
|
||||
"download_again": "دانلود مجدد",
|
||||
"cancel": "لغو",
|
||||
"filter": "فیلتر بازیهای دانلود شده",
|
||||
"remove": "حذف",
|
||||
"downloading_metadata": "در حال دانلود متادیتاها…",
|
||||
"starting_download": "در حال آغار دانلود…",
|
||||
"deleting": "در حال پاک کردن اینستالر…",
|
||||
"delete": "پاک کردن",
|
||||
"remove_from_list": "حذف",
|
||||
"delete_modal_title": "مطمئنی؟",
|
||||
"delete_modal_description": "این کار تمام فایلهای اینستالر را از کامپیوتر شما حذف میکند",
|
||||
"install": "نصف"
|
||||
|
@ -128,8 +105,6 @@
|
|||
"notifications": "نوتیفیکشنها",
|
||||
"enable_download_notifications": "زمانی که یک دانلود تمام شد",
|
||||
"enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد",
|
||||
"telemetry": "تلمتری",
|
||||
"telemetry_description": "فعال کردن آمارگیری استفاده ناشناس",
|
||||
"quit_app_instead_hiding": "به جای کوچک کردن، از هایدرا خارج شو",
|
||||
"launch_with_system": "زمانی که سیستم روشن میشود، هایدرا را باز کن",
|
||||
"general": "کلی",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "En vedette",
|
||||
"recently_added": "Récemment ajouté",
|
||||
"trending": "Tendance",
|
||||
"surprise_me": "Surprenez-moi",
|
||||
"no_results": "Aucun résultat trouvé"
|
||||
|
@ -15,8 +14,7 @@
|
|||
"paused": "{{title}} (En pause)",
|
||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||
"filter": "Filtrer la bibliothèque",
|
||||
"home": "Page d’accueil",
|
||||
"follow_us": "Suivez-nous"
|
||||
"home": "Page d’accueil"
|
||||
},
|
||||
"header": {
|
||||
"search": "Recherche",
|
||||
|
@ -41,7 +39,6 @@
|
|||
"pause": "Pause",
|
||||
"cancel": "Annuler",
|
||||
"remove": "Supprimer",
|
||||
"remove_from_list": "Retirer",
|
||||
"space_left_on_disk": "{{space}} restant sur le disque",
|
||||
"eta": "Fin dans {{eta}}",
|
||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
||||
|
@ -49,12 +46,8 @@
|
|||
"requirements": "Configuration requise",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Recommandée",
|
||||
"no_minimum_requirements": "{{title}} ne fournit pas d'informations sur les configurations minimales",
|
||||
"no_recommended_requirements": "{{title}} ne fournit pas d'informations sur les configurations recommandées",
|
||||
"release_date": "Sorti le {{date}}",
|
||||
"publisher": "Édité par {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copier le lien",
|
||||
"copied_link_to_clipboard": "Lien copié",
|
||||
"hours": "heures",
|
||||
"minutes": "minutes",
|
||||
"amount_hours": "{{amount}} heures",
|
||||
|
@ -87,15 +80,11 @@
|
|||
"eta": "Fin dans {{eta}}",
|
||||
"paused": "En pause",
|
||||
"verifying": "Vérification en cours…",
|
||||
"completed_at": "Terminé en {{date}}",
|
||||
"completed": "Terminé",
|
||||
"download_again": "Télécharger à nouveau",
|
||||
"cancel": "Annuler",
|
||||
"filter": "Filtrer les jeux téléchargés",
|
||||
"remove": "Supprimer",
|
||||
"downloading_metadata": "Téléchargement des métadonnées en cours…",
|
||||
"starting_download": "Démarrage du téléchargement…",
|
||||
"remove_from_list": "Retirer",
|
||||
"delete": "Supprimer le programme d'installation",
|
||||
"delete_modal_description": "Cela supprimera tous les fichiers d'installation de votre ordinateur",
|
||||
"delete_modal_title": "Es-tu sûr?",
|
||||
|
@ -108,8 +97,6 @@
|
|||
"notifications": "Notifications",
|
||||
"enable_download_notifications": "Quand un téléchargement est terminé",
|
||||
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
|
||||
"telemetry": "Télémétrie",
|
||||
"telemetry_description": "Activer les statistiques d'utilisation anonymes",
|
||||
"language": "Langue"
|
||||
},
|
||||
"notifications": {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "Featured",
|
||||
"recently_added": "Nemrég hozzáadott",
|
||||
"trending": "Népszerű",
|
||||
"surprise_me": "Lepj meg",
|
||||
"no_results": "Nem található"
|
||||
|
@ -15,7 +14,6 @@
|
|||
"paused": "{{title}} (Szünet)",
|
||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||
"filter": "Könyvtár szűrése",
|
||||
"follow_us": "Kövess minket",
|
||||
"home": "Főoldal"
|
||||
},
|
||||
"header": {
|
||||
|
@ -46,7 +44,6 @@
|
|||
"pause": "Szüneteltetés",
|
||||
"cancel": "Mégse",
|
||||
"remove": "Eltávolítás",
|
||||
"remove_from_list": "Eltávolítás",
|
||||
"space_left_on_disk": "{{space}} szabad hely a lemezen",
|
||||
"eta": "Befejezés {{eta}}",
|
||||
"downloading_metadata": "Metaadatok letöltése…",
|
||||
|
@ -54,12 +51,8 @@
|
|||
"requirements": "Rendszerkövetelmények",
|
||||
"minimum": "Minimális",
|
||||
"recommended": "Ajánlott",
|
||||
"no_minimum_requirements": "{{title}} nem tartalmaz információt a minimális követelményekről",
|
||||
"no_recommended_requirements": "{{title}} nem tartalmaz információt az ajánlott követelményekről",
|
||||
"release_date": "Megjelenés: {{date}}",
|
||||
"publisher": "Kiadta: {{publisher}}",
|
||||
"copy_link_to_clipboard": "Link másolása",
|
||||
"copied_link_to_clipboard": "Link másolva",
|
||||
"hours": "óra",
|
||||
"minutes": "perc",
|
||||
"amount_hours": "{{amount}} óra",
|
||||
|
@ -95,17 +88,13 @@
|
|||
"eta": "Befejezés {{eta}}",
|
||||
"paused": "Szüneteltetve",
|
||||
"verifying": "Ellenőrzés…",
|
||||
"completed_at": "Befejezve {{date}}-kor",
|
||||
"completed": "Befejezve",
|
||||
"download_again": "Újra letöltés",
|
||||
"cancel": "Mégse",
|
||||
"filter": "Letöltött játékok szűrése",
|
||||
"remove": "Eltávolítás",
|
||||
"downloading_metadata": "Metaadatok letöltése…",
|
||||
"starting_download": "Letöltés indítása…",
|
||||
"deleting": "Telepítő törlése…",
|
||||
"delete": "Telepítő eltávolítása",
|
||||
"remove_from_list": "Eltávolítás",
|
||||
"delete_modal_title": "Biztos vagy benne?",
|
||||
"delete_modal_description": "Ez eltávolít minden telepítési fájlt a számítógépedről",
|
||||
"install": "Telepítés"
|
||||
|
@ -115,9 +104,7 @@
|
|||
"change": "Frissítés",
|
||||
"notifications": "Értesítések",
|
||||
"enable_download_notifications": "Amikor egy letöltés befejeződik",
|
||||
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Névtelen felhasználási statisztikák engedélyezése"
|
||||
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Letöltés befejeződött",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "Unggulan",
|
||||
"recently_added": "Terbaru",
|
||||
"trending": "Trending",
|
||||
"surprise_me": "Kejutkan Saya",
|
||||
"no_results": "Tidak ada hasil"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (Terhenti)",
|
||||
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
||||
"filter": "Filter koleksi",
|
||||
"follow_us": "Ikuti kami",
|
||||
"home": "Beranda",
|
||||
"discord": "Gabung Discord kami",
|
||||
"telegram": "Gabung Telegram kami",
|
||||
"x": "Ikuti akun X kami",
|
||||
"github": "Kontribusi di GitHub"
|
||||
"home": "Beranda"
|
||||
},
|
||||
"header": {
|
||||
"search": "Pencarian",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "Hentikan sementara",
|
||||
"cancel": "Batalkan",
|
||||
"remove": "Hapus",
|
||||
"remove_from_list": "Hapus",
|
||||
"space_left_on_disk": "{{space}} tersisa pada disk",
|
||||
"eta": "Perkiraan {{eta}}",
|
||||
"downloading_metadata": "Mengunduh metadata…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "Keperluan sistem",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Rekomendasi",
|
||||
"no_minimum_requirements": "{{title}} Tidak ada informasi kebutuhan sistem",
|
||||
"no_recommended_requirements": "{{title}} Tidak ada informasi rekomendasi kebutuhan sistem",
|
||||
"release_date": "Dirilis pada {{date}}",
|
||||
"publisher": "Dipublikasikan oleh {{publisher}}",
|
||||
"copy_link_to_clipboard": "Salin tautan",
|
||||
"copied_link_to_clipboard": "Tautan tersalin",
|
||||
"hours": "jam",
|
||||
"minutes": "menit",
|
||||
"amount_hours": "{{amount}} jam",
|
||||
|
@ -83,15 +72,7 @@
|
|||
"change": "Ubah",
|
||||
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
|
||||
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
|
||||
"download_now": "Unduh sekarang",
|
||||
"installation_instructions": "Instruksi Instalasi",
|
||||
"installation_instructions_description": "Langkah tambahan dibutuhkan untuk meng-instal game ini",
|
||||
"online_fix_instruction": "OnlineFix games mebutuhkan kata sandi untuk ekstraksi. Saat diperlukan, gunakan kata sandi ini:",
|
||||
"dodi_installation_instruction": "Saat menjalankan DODI installer, tekan tombol atas pada keyboard <0 /> untuk melanjutkan proses instalasi:",
|
||||
"dont_show_it_again": "Jangan tunjukkan lagi",
|
||||
"copy_to_clipboard": "Salin",
|
||||
"copied_to_clipboard": "Tersalin",
|
||||
"got_it": "Paham"
|
||||
"download_now": "Unduh sekarang"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Aktivasi Hydra",
|
||||
|
@ -107,17 +88,13 @@
|
|||
"eta": "Perkiraan {{eta}}",
|
||||
"paused": "Terhenti sementara",
|
||||
"verifying": "Memeriksa…",
|
||||
"completed_at": "Selesai pada {{date}}",
|
||||
"completed": "Selesai",
|
||||
"download_again": "Unduh lagi",
|
||||
"cancel": "Batalkan",
|
||||
"filter": "Saring game yang diunduh",
|
||||
"remove": "Hapus",
|
||||
"downloading_metadata": "Mengunduh metadata…",
|
||||
"starting_download": "Memulai unduhan…",
|
||||
"deleting": "Menghapus file instalasi…",
|
||||
"delete": "Hapus file instalasi",
|
||||
"remove_from_list": "Hapus",
|
||||
"delete_modal_title": "Kamu yakin?",
|
||||
"delete_modal_description": "Proses ini akan menghapus semua file instalasi dari komputer kamu",
|
||||
"install": "Install"
|
||||
|
@ -128,8 +105,6 @@
|
|||
"notifications": "Pengingat",
|
||||
"enable_download_notifications": "Saat unduhan selesai",
|
||||
"enable_repack_list_notifications": "Saat repack terbaru ditambahkan",
|
||||
"telemetry": "Telemetri",
|
||||
"telemetry_description": "Izinkan statistik penggunaan data anonim",
|
||||
"behavior": "Perilaku",
|
||||
"quit_app_instead_hiding": "Tutup aplikasi alih-alih menyembunyikan aplikasi",
|
||||
"launch_with_system": "Jalankan saat memulai sistem"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "In primo piano",
|
||||
"recently_added": "Aggiunti di recente",
|
||||
"trending": "Di tendenza",
|
||||
"surprise_me": "Sorprendimi",
|
||||
"no_results": "Nessun risultato trovato"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (In pausa)",
|
||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||
"filter": "Filtra libreria",
|
||||
"follow_us": "Seguici",
|
||||
"home": "Home",
|
||||
"discord": "Unisciti al nostro Discord",
|
||||
"telegram": "Unisciti al nostro Telegram",
|
||||
"x": "Segui su X",
|
||||
"github": "Contribuisci su GitHub"
|
||||
"home": "Home"
|
||||
},
|
||||
"header": {
|
||||
"search": "Cerca",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "Metti in pausa",
|
||||
"cancel": "Annulla",
|
||||
"remove": "Rimuovi",
|
||||
"remove_from_list": "Rimuovi",
|
||||
"space_left_on_disk": "{{space}} rimasto sul disco",
|
||||
"eta": "Conclusione {{eta}}",
|
||||
"downloading_metadata": "Scaricamento metadati…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "Requisiti di sistema",
|
||||
"minimum": "Minimi",
|
||||
"recommended": "Consigliati",
|
||||
"no_minimum_requirements": "{{title}} non fornisce informazioni sui requisiti minimi",
|
||||
"no_recommended_requirements": "{{title}} non fornisce informazioni sui requisiti consigliati",
|
||||
"release_date": "Rilasciato il {{date}}",
|
||||
"publisher": "Pubblicato da {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copia link",
|
||||
"copied_link_to_clipboard": "Link copiato",
|
||||
"hours": "ore",
|
||||
"minutes": "minuti",
|
||||
"amount_hours": "{{amount}} ore",
|
||||
|
@ -84,14 +73,6 @@
|
|||
"repacks_modal_description": "Scegli il repack che vuoi scaricare",
|
||||
"select_folder_hint": "Per cambiare la cartella predefinita, accedi alle",
|
||||
"download_now": "Scarica ora",
|
||||
"installation_instructions": "Istruzioni di installazione",
|
||||
"installation_instructions_description": "Sono necessari passaggi aggiuntivi per installare questo gioco",
|
||||
"online_fix_instruction": "I giochi OnlineFix richiedono una password per essere estratti. Quando richiesto, utilizza la seguente password:",
|
||||
"dodi_installation_instruction": "Quando apri l'installatore di DODI, premi il tasto su della tua tastiera <0 /> per avviare il processo di installazione:",
|
||||
"dont_show_it_again": "Non mostrarlo più",
|
||||
"copy_to_clipboard": "Copia",
|
||||
"copied_to_clipboard": "Copiato",
|
||||
"got_it": "Capito",
|
||||
"no_shop_details": "Impossibile recuperare i dettagli del negozio.",
|
||||
"download_options": "Opzioni di download",
|
||||
"download_path": "Percorso di download",
|
||||
|
@ -114,17 +95,13 @@
|
|||
"eta": "Conclusione {{eta}}",
|
||||
"paused": "In pausa",
|
||||
"verifying": "Verifica…",
|
||||
"completed_at": "Completato in {{date}}",
|
||||
"completed": "Completato",
|
||||
"download_again": "Scarica di nuovo",
|
||||
"cancel": "Annulla",
|
||||
"filter": "Filtra giochi scaricati",
|
||||
"remove": "Rimuovi",
|
||||
"downloading_metadata": "Scaricamento metadati…",
|
||||
"starting_download": "Avvio download…",
|
||||
"deleting": "Eliminazione dell'installer…",
|
||||
"delete": "Rimuovi installer",
|
||||
"remove_from_list": "Rimuovi",
|
||||
"delete_modal_title": "Sei sicuro?",
|
||||
"delete_modal_description": "Questo rimuoverà tutti i file di installazione dal tuo computer",
|
||||
"install": "Installa"
|
||||
|
@ -135,8 +112,6 @@
|
|||
"notifications": "Notifiche",
|
||||
"enable_download_notifications": "Quando un download è completo",
|
||||
"enable_repack_list_notifications": "Quando viene aggiunto un nuovo repack",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Abilita statistiche di utilizzo anonime",
|
||||
"real_debrid_api_token_label": "Token API Real Debrid",
|
||||
"quit_app_instead_hiding": "Esci da Hydra invece di nascondere nell'area di notifica",
|
||||
"launch_with_system": "Apri Hydra all'avvio",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "추천",
|
||||
"recently_added": "최근 추가됨",
|
||||
"trending": "인기",
|
||||
"surprise_me": "무작위 추천",
|
||||
"no_results": "결과 없음"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (일시 정지됨)",
|
||||
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
||||
"filter": "라이브러리 정렬",
|
||||
"follow_us": "공식 SNS",
|
||||
"home": "홈",
|
||||
"discord": "공식 디스코드",
|
||||
"telegram": "공식 텔레그램",
|
||||
"x": "공식 X (구 트위터)",
|
||||
"github": "GitHub에서 기여하기"
|
||||
"home": "홈"
|
||||
},
|
||||
"header": {
|
||||
"search": "게임 검색하기",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "일시 정지",
|
||||
"cancel": "취소",
|
||||
"remove": "제거",
|
||||
"remove_from_list": "목록에서 제거",
|
||||
"space_left_on_disk": "여유 저장 용량 {{space}} 남음",
|
||||
"eta": "완료까지 {{eta}}",
|
||||
"downloading_metadata": "메타데이터 다운로드 중…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "시스템 사양",
|
||||
"minimum": "최저 사양",
|
||||
"recommended": "권장 사양",
|
||||
"no_minimum_requirements": "{{title}}의 최저 사양을 제공받지 못 함",
|
||||
"no_recommended_requirements": "{{title}}의 권장 사양을 제공받지 못 함",
|
||||
"release_date": "{{date}}에 발매됨",
|
||||
"publisher": "{{publisher}} 배급",
|
||||
"copy_link_to_clipboard": "링크 복사하기",
|
||||
"copied_link_to_clipboard": "링크 복사됨",
|
||||
"hours": "시",
|
||||
"minutes": "분",
|
||||
"amount_hours": "{{amount}} 시간",
|
||||
|
@ -83,15 +72,7 @@
|
|||
"change": "바꾸기",
|
||||
"repacks_modal_description": "다운로드 할 리팩을 선택해 주세요",
|
||||
"select_folder_hint": "기본 폴더를 바꾸려면 <0>설정</0>으로 가세요",
|
||||
"download_now": "지금 다운로드",
|
||||
"installation_instructions": "설치 방법",
|
||||
"installation_instructions_description": "이 게임을 설치하기 위해서는 추가적인 단계가 필요합니다",
|
||||
"online_fix_instruction": "OnlineFix 게임들은 압축 해제 시 암호가 필요합니다. 비밀번호를 물을 때 다음을 암호로 사용하기:",
|
||||
"dodi_installation_instruction": "DODI 인스톨러를 실행했다면 키보드의 위 방향키를 눌러 설치를 시작하세요:",
|
||||
"dont_show_it_again": "다시 보지 않기",
|
||||
"copy_to_clipboard": "복사하기",
|
||||
"copied_to_clipboard": "복사됨",
|
||||
"got_it": "알았습니다"
|
||||
"download_now": "지금 다운로드"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra 실행",
|
||||
|
@ -107,17 +88,13 @@
|
|||
"eta": "완료까지 {{eta}}",
|
||||
"paused": "일시 정지됨",
|
||||
"verifying": "검증중…",
|
||||
"completed_at": "{{date}}에 완료됨",
|
||||
"completed": "완료됨",
|
||||
"download_again": "다시 다운로드 하기",
|
||||
"cancel": "취소",
|
||||
"filter": "다운로드 된 게임들을 정렬하기",
|
||||
"remove": "제거하기",
|
||||
"downloading_metadata": "메타데이터 다운로드 중…",
|
||||
"starting_download": "다운로드 개시 중…",
|
||||
"deleting": "인스톨러 삭제 중…",
|
||||
"delete": "인스톨러 삭제하기",
|
||||
"remove_from_list": "제거하기",
|
||||
"delete_modal_title": "정말로 하시겠습니까?",
|
||||
"delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다",
|
||||
"install": "설치"
|
||||
|
@ -128,8 +105,6 @@
|
|||
"notifications": "알림",
|
||||
"enable_download_notifications": "다운로드가 완료되었을 때",
|
||||
"enable_repack_list_notifications": "새 리팩이 추가되었을 때",
|
||||
"telemetry": "자동 데이터 수집",
|
||||
"telemetry_description": "익명 사용 통계를 활성화",
|
||||
"quit_app_instead_hiding": "작업 표시줄 트레이로 최소화하는 대신 Hydra를 종료",
|
||||
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
|
||||
"general": "일반",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "Uitgelicht",
|
||||
"recently_added": "Recent Toegevoegd",
|
||||
"trending": "Trending",
|
||||
"surprise_me": "Verrasing",
|
||||
"no_results": "Geen resultaten gevonden"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (Gepauzeerd)",
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filter Bibliotheek",
|
||||
"follow_us": "volg ons",
|
||||
"home": "Home",
|
||||
"discord": "Volg onze Discord",
|
||||
"telegram": "Volg onze Telegram",
|
||||
"x": "Volg ons op X",
|
||||
"github": "Contribute op GitHub"
|
||||
"home": "Home"
|
||||
},
|
||||
"header": {
|
||||
"search": "Zoek spellen",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "Pauze",
|
||||
"cancel": "Stoppen",
|
||||
"remove": "Verwijderen",
|
||||
"remove_from_list": "Verwijdere van lijst",
|
||||
"space_left_on_disk": "{{space}} Over op schijf",
|
||||
"eta": "Conclusie {{eta}}",
|
||||
"downloading_metadata": "Downloading metadata…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "Systeem vereisten",
|
||||
"minimum": "Minimaal",
|
||||
"recommended": "Aanbevolen",
|
||||
"no_minimum_requirements": "{{title}} biedt geen informatie over de minimale vereisten",
|
||||
"no_recommended_requirements": "{{title}} biedt geen informatie over aanbevolen vereisten",
|
||||
"release_date": "Uitgebracht op {{date}}",
|
||||
"publisher": "Gepubliceerd door {{publisher}}",
|
||||
"copy_link_to_clipboard": "Kopieer link",
|
||||
"copied_link_to_clipboard": "Link Gekopieerd",
|
||||
"hours": "uren",
|
||||
"minutes": "minuten",
|
||||
"amount_hours": "{{amount}} uren",
|
||||
|
@ -83,15 +72,7 @@
|
|||
"change": "Verander",
|
||||
"repacks_modal_description": "Kies de herverpakking die u wilt downloaden",
|
||||
"select_folder_hint": "Om de standaardmap te wijzigen, gaat u naar <0>instellingen</0>",
|
||||
"download_now": "Download nu",
|
||||
"installation_instructions": "Installatie instructies",
|
||||
"installation_instructions_description": "Er zijn extra stappen vereist om deze game te installeren",
|
||||
"online_fix_instruction": "OnlineFix-spellen vereisen dat een wachtwoord wordt uitgepakt. Gebruik indien nodig het volgende wachtwoord:",
|
||||
"dodi_installation_instruction": "Wanneer u het DODI-installatieprogramma opent, drukt u op de toets omhoog <0 /> op uw toetsenbord om het installatieproces te starten:",
|
||||
"dont_show_it_again": "Laat het niet meer zien",
|
||||
"copy_to_clipboard": "Kopiëren",
|
||||
"copied_to_clipboard": "Gekopieerd",
|
||||
"got_it": "Begrepen"
|
||||
"download_now": "Download nu"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activeer Hydra",
|
||||
|
@ -107,17 +88,13 @@
|
|||
"eta": "Conclusie{{eta}}",
|
||||
"paused": "Gepauzeerd",
|
||||
"verifying": "Verifiëren…",
|
||||
"completed_at": "Voltooid binnen {{date}}",
|
||||
"completed": "Voltooid",
|
||||
"download_again": "Opnieuw downloaden",
|
||||
"cancel": "Annuleren",
|
||||
"filter": "Filter gedownloade games",
|
||||
"remove": "Verwijderen",
|
||||
"downloading_metadata": "Metagegevens downloaden",
|
||||
"starting_download": "download starten",
|
||||
"deleting": "Installatieprogramma verwijderen…",
|
||||
"delete": "Installatieprogramma verwijderen",
|
||||
"remove_from_list": "Verwijderen",
|
||||
"delete_modal_title": "Weet je het zeker?",
|
||||
"delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd",
|
||||
"install": "Installeren"
|
||||
|
@ -128,8 +105,6 @@
|
|||
"notifications": "Meldingen",
|
||||
"enable_download_notifications": "Wanneer een download voltooid is",
|
||||
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
|
||||
"telemetry": "Telemetrie",
|
||||
"telemetry_description": "Schakel anonieme gebruiksstatistieken in",
|
||||
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||
"quit_app_instead_hiding": "Sluit Hydra af in plaats van te minimaliseren naar de lade",
|
||||
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "Wyróżnione",
|
||||
"recently_added": "Ostatnio dodane",
|
||||
"trending": "Trendujące",
|
||||
"surprise_me": "Zaskocz mnie",
|
||||
"no_results": "Nie znaleziono wyników"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (Zatrzymano)",
|
||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||
"filter": "Filtruj biblioteke",
|
||||
"follow_us": "Śledź nas",
|
||||
"home": "Główna",
|
||||
"discord": "Dołącz nasz Discord",
|
||||
"telegram": "Dołącz nasz Telegram",
|
||||
"x": "Śledź na X",
|
||||
"github": "Przyczyń się na GitHub"
|
||||
"home": "Główna"
|
||||
},
|
||||
"header": {
|
||||
"search": "Szukaj",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "Zatrzymaj",
|
||||
"cancel": "Anuluj",
|
||||
"remove": "Usuń",
|
||||
"remove_from_list": "Usuń",
|
||||
"space_left_on_disk": "{{space}} wolnego na dysku",
|
||||
"eta": "Podsumowanie {{eta}}",
|
||||
"downloading_metadata": "Pobieranie metadata…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "Wymagania systemowe",
|
||||
"minimum": "Minimalne",
|
||||
"recommended": "Zalecane",
|
||||
"no_minimum_requirements": "{{title}} nie zawiera informacji o minimalnych wymaganiach",
|
||||
"no_recommended_requirements": "{{title}} nie zawiera informacji o zalecanych wymaganiach",
|
||||
"release_date": "Wydano w {{date}}",
|
||||
"publisher": "Opublikowany przez {{publisher}}",
|
||||
"copy_link_to_clipboard": "Kopiuj łącze",
|
||||
"copied_link_to_clipboard": "Skopiowano łącze",
|
||||
"hours": "godzin",
|
||||
"minutes": "minut",
|
||||
"amount_hours": "{{amount}} godzin",
|
||||
|
@ -84,14 +73,6 @@
|
|||
"repacks_modal_description": "Wybierz repack, który chcesz pobrać",
|
||||
"select_folder_hint": "Aby zmienić domyślny folder, przejdź do",
|
||||
"download_now": "Pobierz teraz",
|
||||
"installation_instructions": "Instrukcja instalacji",
|
||||
"installation_instructions_description": "Do zainstalowania tej gry wymagane są dodatkowe kroki",
|
||||
"online_fix_instruction": "Gry OnlineFix wymagają hasła do wyodrębnienia. W razie potrzeby użyj następującego hasła:",
|
||||
"dodi_installation_instruction": "Po otwarciu instalatora DODI naciśnij klawisz <0 /> w górę, aby rozpocząć proces instalacji:",
|
||||
"dont_show_it_again": "Nie pokazuj tego ponownie",
|
||||
"copy_to_clipboard": "Skopiuj",
|
||||
"copied_to_clipboard": "Skopiowano",
|
||||
"got_it": "Rozumiem",
|
||||
"no_shop_details": "Nie udało się pobrać danych sklepu.",
|
||||
"download_options": "Opcje pobierania",
|
||||
"download_path": "Ścieżka pobierania",
|
||||
|
@ -114,17 +95,13 @@
|
|||
"eta": "Podsumowanie {{eta}}",
|
||||
"paused": "Zatrzymano",
|
||||
"verifying": "Weryfikowanie…",
|
||||
"completed_at": "Zakończono w {{date}}",
|
||||
"completed": "Zakończono",
|
||||
"download_again": "Pobierz ponownie",
|
||||
"cancel": "Anuluj",
|
||||
"filter": "Filtruj pobrane gry",
|
||||
"remove": "Usuń",
|
||||
"downloading_metadata": "Pobieranie metadata…",
|
||||
"starting_download": "Rozpoczęto pobieranie…",
|
||||
"deleting": "Usuwanie instalatora…",
|
||||
"delete": "Usuń instalator",
|
||||
"remove_from_list": "Usuń",
|
||||
"delete_modal_title": "Czy na pewno?",
|
||||
"delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera",
|
||||
"install": "Instaluj"
|
||||
|
@ -135,8 +112,6 @@
|
|||
"notifications": "Powiadomienia",
|
||||
"enable_download_notifications": "Gdy pobieranie zostanie zakończone",
|
||||
"enable_repack_list_notifications": "Gdy dodawany jest nowy repack",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Włącz anonimowe statystyki użycia",
|
||||
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||
"quit_app_instead_hiding": "Zamknij Hydr zamiast minimalizować do zasobnika",
|
||||
"launch_with_system": "Uruchom Hydra przy starcie systemu",
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"app": {
|
||||
"successfully_signed_in": "Logado com sucesso"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destaque",
|
||||
"trending": "Populares",
|
||||
|
@ -15,9 +18,9 @@
|
|||
"downloading": "{{title}} ({{percentage}} - Baixando…)",
|
||||
"filter": "Filtrar biblioteca",
|
||||
"home": "Início",
|
||||
"follow_us": "Acompanhe-nos",
|
||||
"queued": "{{title}} (Na fila)",
|
||||
"game_has_no_executable": "Jogo não possui executável selecionado"
|
||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||
"sign_in": "Login"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
|
@ -45,7 +48,6 @@
|
|||
"pause": "Pausar",
|
||||
"cancel": "Cancelar",
|
||||
"remove": "Remover",
|
||||
"remove_from_list": "Remover",
|
||||
"space_left_on_disk": "{{space}} livres em disco",
|
||||
"eta": "Conclusão {{eta}}",
|
||||
"calculating_eta": "Calculando tempo restante…",
|
||||
|
@ -54,13 +56,9 @@
|
|||
"requirements": "Requisitos do sistema",
|
||||
"minimum": "Mínimos",
|
||||
"recommended": "Recomendados",
|
||||
"no_minimum_requirements": "{{title}} não possui informações de requisitos mínimos",
|
||||
"no_recommended_requirements": "{{title}} não possui informações de requisitos recomendados",
|
||||
"paused": "Pausado",
|
||||
"release_date": "Lançado em {{date}}",
|
||||
"publisher": "Publicado por {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copiar link",
|
||||
"copied_link_to_clipboard": "Link copiado",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos",
|
||||
"amount_hours": "{{amount}} horas",
|
||||
|
@ -82,14 +80,6 @@
|
|||
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
|
||||
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes</0>",
|
||||
"download_now": "Iniciar download",
|
||||
"installation_instructions": "Instruções de Instalação",
|
||||
"installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo",
|
||||
"online_fix_instruction": "Jogos OnlineFix precisam de uma senha para serem extraídos. Quando solicitado, utilize a seguinte senha:",
|
||||
"dodi_installation_instruction": "Quando o instalador do DODI for aberto, pressione a seta para cima <0 /> do teclado para iniciar o processo de instalação:",
|
||||
"dont_show_it_again": "Não mostrar novamente",
|
||||
"copy_to_clipboard": "Copiar",
|
||||
"copied_to_clipboard": "Copiado",
|
||||
"got_it": "Entendi",
|
||||
"no_shop_details": "Não foi possível obter os detalhes da loja.",
|
||||
"download_options": "Opções de download",
|
||||
"download_path": "Diretório de download",
|
||||
|
@ -116,7 +106,9 @@
|
|||
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra",
|
||||
"download_in_progress": "Download em andamento",
|
||||
"download_paused": "Download pausado",
|
||||
"last_downloaded_option": "Última opção baixada"
|
||||
"last_downloaded_option": "Última opção baixada",
|
||||
"create_shortcut_success": "Atalho criado com sucesso",
|
||||
"create_shortcut_error": "Erro ao criar atalho"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
|
@ -132,16 +124,12 @@
|
|||
"eta": "Conclusão {{eta}}",
|
||||
"paused": "Pausado",
|
||||
"verifying": "Verificando…",
|
||||
"completed_at": "Concluído em {{date}}",
|
||||
"completed": "Concluído",
|
||||
"removed": "Cancelado",
|
||||
"download_again": "Baixar novamente",
|
||||
"cancel": "Cancelar",
|
||||
"filter": "Filtrar jogos baixados",
|
||||
"remove": "Remover",
|
||||
"downloading_metadata": "Baixando metadados…",
|
||||
"starting_download": "Iniciando download…",
|
||||
"remove_from_list": "Remover",
|
||||
"delete": "Remover instalador",
|
||||
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
||||
"delete_modal_title": "Tem certeza?",
|
||||
|
@ -160,8 +148,6 @@
|
|||
"notifications": "Notificações",
|
||||
"enable_download_notifications": "Quando um download for concluído",
|
||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Habilitar estatísticas de uso anônimas",
|
||||
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
||||
"quit_app_instead_hiding": "Encerrar o Hydra ao invés de minimizá-lo ao fechar",
|
||||
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
||||
|
@ -195,7 +181,12 @@
|
|||
"sync_download_sources": "Sincronizar",
|
||||
"removed_download_source": "Fonte removida",
|
||||
"added_download_source": "Fonte adicionada",
|
||||
"download_sources_synced": "As fontes foram sincronizadas"
|
||||
"download_sources_synced": "As fontes foram sincronizadas",
|
||||
"insert_valid_json_url": "Insira a url de um JSON válido",
|
||||
"found_download_option_zero": "Nenhuma opção de download encontrada",
|
||||
"found_download_option_one": "{{countFormatted}} opção de download encontrada",
|
||||
"found_download_option_other": "{{countFormatted}} opções de download encontradas",
|
||||
"import": "Importar"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
|
@ -225,5 +216,27 @@
|
|||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Alternar visibilidade da senha"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"last_time_played": "Jogou {{period}}",
|
||||
"activity": "Atividade recente",
|
||||
"library": "Biblioteca",
|
||||
"total_play_time": "Tempo total de jogo: {{amount}}",
|
||||
"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",
|
||||
"saving": "Salvando…",
|
||||
"save": "Salvar",
|
||||
"edit_profile": "Editar Perfil",
|
||||
"saved_successfully": "Salvo com sucesso",
|
||||
"try_again": "Por favor, tente novamente",
|
||||
"cancel": "Cancelar",
|
||||
"successfully_signed_out": "Deslogado com sucesso",
|
||||
"sign_out": "Sair da conta",
|
||||
"sign_out_modal_title": "Tem certeza?",
|
||||
"playing_for": "Jogando por {{amount}}",
|
||||
"sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"app": {
|
||||
"successfully_signed_in": "Успешный вход"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Рекомендованное",
|
||||
"trending": "В тренде",
|
||||
|
@ -16,7 +19,8 @@
|
|||
"filter": "Фильтр библиотеки",
|
||||
"home": "Главная",
|
||||
"queued": "{{title}} (В очереди)",
|
||||
"game_has_no_executable": "Файл запуска игры не выбран"
|
||||
"game_has_no_executable": "Файл запуска игры не выбран",
|
||||
"sign_in": "Войти"
|
||||
},
|
||||
"header": {
|
||||
"search": "Поиск",
|
||||
|
@ -49,7 +53,6 @@
|
|||
"pause": "Приостановить",
|
||||
"cancel": "Отменить",
|
||||
"remove": "Удалить",
|
||||
"remove_from_list": "Удалить",
|
||||
"space_left_on_disk": "{{space}} свободно на диске",
|
||||
"eta": "Окончание {{eta}}",
|
||||
"calculating_eta": "Подсчёт оставшегося времени…",
|
||||
|
@ -58,13 +61,9 @@
|
|||
"requirements": "Системные требования",
|
||||
"minimum": "Минимальные",
|
||||
"recommended": "Рекомендуемые",
|
||||
"no_minimum_requirements": "Для {{title}} не указаны минимальные требования",
|
||||
"no_recommended_requirements": "Для {{title}} не указаны рекомендуемые требования",
|
||||
"paused": "Приостановлено",
|
||||
"release_date": "Выпущено {{date}}",
|
||||
"publisher": "Издатель {{publisher}}",
|
||||
"copy_link_to_clipboard": "Копировать ссылку",
|
||||
"copied_link_to_clipboard": "Ссылка скопирована",
|
||||
"hours": "часов",
|
||||
"minutes": "минут",
|
||||
"amount_hours": "{{amount}} часов",
|
||||
|
@ -85,14 +84,6 @@
|
|||
"repacks_modal_description": "Выберите репак для загрузки",
|
||||
"select_folder_hint": "Чтобы изменить папку загрузок по умолчанию, откройте <0>Настройки</0>",
|
||||
"download_now": "Загрузить сейчас",
|
||||
"installation_instructions": "Инструкция по установке",
|
||||
"installation_instructions_description": "Для установки этой игры требуются дополнительные шаги",
|
||||
"online_fix_instruction": "В играх с OnlineFix требуется ввести пароль для извлечения. При необходимости используйте следующий пароль:",
|
||||
"dodi_installation_instruction": "Когда вы откроете установщик DODI, нажмите на клавиатуре клавишу 'вверх' <0 />, чтобы начать процесс установки:",
|
||||
"dont_show_it_again": "Не показывать снова",
|
||||
"copy_to_clipboard": "Копировать",
|
||||
"copied_to_clipboard": "Скопировано",
|
||||
"got_it": "Понятно",
|
||||
"no_shop_details": "Не удалось получить описание",
|
||||
"download_options": "Вариантов загрузки",
|
||||
"download_path": "Путь для загрузок",
|
||||
|
@ -119,7 +110,9 @@
|
|||
"danger_zone_section_description": "Удалить эту игру из вашей библиотеки или файлы скачанные Hydra",
|
||||
"download_in_progress": "Идёт загрузка",
|
||||
"download_paused": "Загрузка приостановлена",
|
||||
"last_downloaded_option": "Последний вариант загрузки"
|
||||
"last_downloaded_option": "Последний вариант загрузки",
|
||||
"create_shortcut_success": "Ярлык создан",
|
||||
"create_shortcut_error": "Не удалось создать ярлык"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активировать Hydra",
|
||||
|
@ -135,18 +128,14 @@
|
|||
"eta": "Окончание {{eta}}",
|
||||
"paused": "Приостановлено",
|
||||
"verifying": "Проверка…",
|
||||
"completed_at": "Завершено в {{date}}",
|
||||
"completed": "Завершено",
|
||||
"removed": "Не скачано",
|
||||
"download_again": "Загрузить снова",
|
||||
"cancel": "Отмена",
|
||||
"filter": "Фильтр загруженных игр",
|
||||
"remove": "Удалить",
|
||||
"downloading_metadata": "Загрузка метаданных…",
|
||||
"starting_download": "Начало загрузки…",
|
||||
"deleting": "Удаление установщика…",
|
||||
"delete": "Удалить установщик",
|
||||
"remove_from_list": "Удалить",
|
||||
"delete_modal_title": "Вы уверены?",
|
||||
"delete_modal_description": "Это удалит все установщики с вашего компьютера",
|
||||
"install": "Установить",
|
||||
|
@ -163,14 +152,12 @@
|
|||
"notifications": "Уведомления",
|
||||
"enable_download_notifications": "По завершении загрузки",
|
||||
"enable_repack_list_notifications": "При добавлении нового репака",
|
||||
"telemetry": "Телеметрия",
|
||||
"telemetry_description": "Отправлять анонимную статистику использования",
|
||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||
"quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей",
|
||||
"launch_with_system": "Запуск Hydra вместе с системой",
|
||||
"general": "Основные",
|
||||
"behavior": "Поведение",
|
||||
"download_sources": "Скачать исходный код",
|
||||
"download_sources": "Источники загрузки",
|
||||
"language": "Язык",
|
||||
"real_debrid_api_token": "API Ключ",
|
||||
"enable_real_debrid": "Включить Real-Debrid",
|
||||
|
@ -198,7 +185,12 @@
|
|||
"sync_download_sources": "Синхронизировать источники",
|
||||
"removed_download_source": "Источник загрузок удален",
|
||||
"added_download_source": "Источник загрузок добавлен",
|
||||
"download_sources_synced": "Все источники загрузок синхронизированы"
|
||||
"download_sources_synced": "Все источники загрузок синхронизированы",
|
||||
"insert_valid_json_url": "Вставьте действительный URL JSON-файла",
|
||||
"found_download_option_zero": "Не найдено вариантов загрузки",
|
||||
"found_download_option_one": "Найден {{countFormatted}} вариант загрузки",
|
||||
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
||||
"import": "Импортировать"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
|
@ -224,5 +216,27 @@
|
|||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Показывать пароль"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} часов",
|
||||
"amount_minutes": "{{amount}} минут",
|
||||
"last_time_played": "Последняя игра {{period}}",
|
||||
"activity": "Недавняя активность",
|
||||
"library": "Библиотека",
|
||||
"total_play_time": "Всего сыграно: {{amount}}",
|
||||
"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": "Ваша библиотека связана с текущей учетной записью. При выходе из системы ваша библиотека станет недоступна, и прогресс не будет сохранен. Выйти?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"featured": "Öne çıkan",
|
||||
"recently_added": "Son eklenen",
|
||||
"trending": "Popüler",
|
||||
"surprise_me": "Şaşırt beni",
|
||||
"no_results": "Sonuç bulunamadı"
|
||||
|
@ -15,12 +14,7 @@
|
|||
"paused": "{{title}} (Duraklatıldı)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
"filter": "Kütüphaneyi filtrele",
|
||||
"follow_us": "Bizi takip et",
|
||||
"home": "Ana menü",
|
||||
"discord": "Discord'umuza katıl",
|
||||
"telegram": "Telegram'umuza katıl",
|
||||
"x": "X'te bizi takip et",
|
||||
"github": "GitHub'da bize katkı yap"
|
||||
"home": "Ana menü"
|
||||
},
|
||||
"header": {
|
||||
"search": "Ara",
|
||||
|
@ -50,7 +44,6 @@
|
|||
"pause": "Duraklat",
|
||||
"cancel": "İptal et",
|
||||
"remove": "Sil",
|
||||
"remove_from_list": "Sil",
|
||||
"space_left_on_disk": "Diskte {{space}} yer kaldı",
|
||||
"eta": "Bitiş {{eta}}",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
|
@ -58,12 +51,8 @@
|
|||
"requirements": "Sistem gereksinimleri",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Önerilen",
|
||||
"no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor",
|
||||
"no_recommended_requirements": "{{title}} önerilen sistem gereksinim bilgilerini karşılamıyor",
|
||||
"release_date": "{{date}} tarihinde çıktı",
|
||||
"publisher": "{{publisher}} tarihinde yayınlandı",
|
||||
"copy_link_to_clipboard": "Link'i kopyala",
|
||||
"copied_link_to_clipboard": "Link kopyalandı",
|
||||
"hours": "saatler",
|
||||
"minutes": "dakikalar",
|
||||
"amount_hours": "{{amount}} saat",
|
||||
|
@ -83,15 +72,7 @@
|
|||
"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",
|
||||
"installation_instructions": "Kurulum",
|
||||
"installation_instructions_description": "Bu oyunu kurmak için ek adımlar gerekiyor",
|
||||
"online_fix_instruction": "OnlineFix oyunlarını ayıklamak için parola gerekiyor. Gerekli olduğunda bu parolayı kullanın:",
|
||||
"dodi_installation_instruction": "Dodi installerını açtığınızda, kurulumu başlatmak için bu tuşa basın <0 />:",
|
||||
"dont_show_it_again": "Tekrar gösterme",
|
||||
"copy_to_clipboard": "Kopyala",
|
||||
"copied_to_clipboard": "Kopyalandı",
|
||||
"got_it": "Tamam"
|
||||
"download_now": "Şimdi"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'yı aktif et",
|
||||
|
@ -107,17 +88,13 @@
|
|||
"eta": "Bitiş {{eta}}",
|
||||
"paused": "Duraklatıldı",
|
||||
"verifying": "Doğrulanıyor…",
|
||||
"completed_at": "{{date}} tarihinde tamamlanacak",
|
||||
"completed": "Tamamlandı",
|
||||
"download_again": "Tekrar indir",
|
||||
"cancel": "İptal et",
|
||||
"filter": "Yüklü oyunları filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"starting_download": "İndirme başlatılıyor…",
|
||||
"deleting": "Installer siliniyor…",
|
||||
"delete": "Installer'ı sil",
|
||||
"remove_from_list": "Kaldır",
|
||||
"delete_modal_title": "Emin misiniz?",
|
||||
"delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek",
|
||||
"install": "Kur"
|
||||
|
@ -127,9 +104,7 @@
|
|||
"change": "Güncelle",
|
||||
"notifications": "Bildirimler",
|
||||
"enable_download_notifications": "Bir indirme bittiğinde",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
|
||||
"telemetry": "Telemetri",
|
||||
"telemetry_description": "Anonim kullanım istatistiklerini aktifleştir"
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "İndirme tamamlandı",
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"app": {
|
||||
"successfully_signed_in": "Успішний вхід в систему"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Рекомендоване",
|
||||
"recently_added": "Нове",
|
||||
"trending": "У тренді",
|
||||
"surprise_me": "Здивуй мене",
|
||||
"no_results": "Результатів не знайдено"
|
||||
|
@ -15,12 +17,10 @@
|
|||
"paused": "{{title}} (Призупинено)",
|
||||
"downloading": "{{title}} ({{percentage}} - Завантаження…)",
|
||||
"filter": "Фільтр бібліотеки",
|
||||
"follow_us": "Підписуйтесь на нас",
|
||||
"home": "Головна",
|
||||
"discord": "Приєднуйтесь до Discord",
|
||||
"telegram": "Приєднуйтесь до Telegram",
|
||||
"x": "Підписуйтесь на X",
|
||||
"github": "Зробіть свій внесок на GitHub"
|
||||
"game_has_no_executable": "Не було вибрано файл для запуску гри",
|
||||
"queued": "{{title}} в черзі",
|
||||
"sign_in": "Увійти"
|
||||
},
|
||||
"header": {
|
||||
"search": "Пошук",
|
||||
|
@ -28,12 +28,15 @@
|
|||
"catalogue": "Каталог",
|
||||
"downloads": "Завантаження",
|
||||
"search_results": "Результати пошуку",
|
||||
"settings": "Налаштування"
|
||||
"settings": "Налаштування",
|
||||
"version_available_download": "Доступна версія {{version}}. Натисніть тут, щоб перезапустити та встановити.",
|
||||
"version_available_install": "Доступна версія {{version}}. Натисніть тут для завантаження."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Немає активних завантажень",
|
||||
"downloading_metadata": "Завантаження метаданих {{title}}…",
|
||||
"downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}"
|
||||
"downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Завантаження {{title}}… ({{percentage}} завершено) - Обчислення залишкового часу…"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Наступна сторінка",
|
||||
|
@ -50,7 +53,6 @@
|
|||
"pause": "Призупинити",
|
||||
"cancel": "Скасувати",
|
||||
"remove": "Видалити",
|
||||
"remove_from_list": "Видалити",
|
||||
"space_left_on_disk": "{{space}} вільно на диску",
|
||||
"eta": "Закінчення {{eta}}",
|
||||
"downloading_metadata": "Завантаження метаданих…",
|
||||
|
@ -58,12 +60,8 @@
|
|||
"requirements": "Системні вимоги",
|
||||
"minimum": "Мінімальні",
|
||||
"recommended": "Рекомендовані",
|
||||
"no_minimum_requirements": "Для {{title}} не вказані мінімальні вимоги",
|
||||
"no_recommended_requirements": "Для {{title}} не вказані рекомендовані вимоги",
|
||||
"release_date": "Випущено {{date}}",
|
||||
"publisher": "Видавець {{publisher}}",
|
||||
"copy_link_to_clipboard": "Скопіювати посилання",
|
||||
"copied_link_to_clipboard": "Посилання скопійовано",
|
||||
"hours": "годин",
|
||||
"minutes": "хвилин",
|
||||
"amount_hours": "{{amount}} годин",
|
||||
|
@ -84,20 +82,41 @@
|
|||
"repacks_modal_description": "Виберіть репак, який хочете завантажити",
|
||||
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
|
||||
"download_now": "Завантажити зараз",
|
||||
"installation_instructions": "Інструкція зі встановлення",
|
||||
"installation_instructions_description": "Для встановлення цієї гри потрібні додаткові кроки",
|
||||
"online_fix_instruction": "В іграх з OnlineFix потрібно ввести пароль для вилучення. За необхідності використовуйте наступний пароль:",
|
||||
"dodi_installation_instruction": "Коли ви відкриєте інсталятор DODI, натисніть на клавіатурі клавішу 'вгору' <0 />, щоб почати процес встановлення:",
|
||||
"dont_show_it_again": "Не показувати це знову",
|
||||
"copy_to_clipboard": "Копіювати",
|
||||
"copied_to_clipboard": "Скопійовано",
|
||||
"got_it": "Зрозуміло"
|
||||
"calculating_eta": "Обчислення залишкового часу…",
|
||||
"create_shortcut": "Створити ярлик на робочому столі",
|
||||
"danger_zone_section_description": "Видалити цю гру з вашої бібліотеки або файли скачані Hydra",
|
||||
"danger_zone_section_title": "Небезпечна зона",
|
||||
"download_in_progress": "Триває завантаження.",
|
||||
"download_options": "Варіантів завантаження",
|
||||
"download_path": "Тека для завантажень",
|
||||
"download_paused": "Завантаження призупинено",
|
||||
"download_settings": "Налаштування завантаження",
|
||||
"downloader": "Завантажувач",
|
||||
"downloads_secion_title": "Завантаження",
|
||||
"downloads_section_description": "Перевірити наявність оновлень або інших версій гри",
|
||||
"executable_section_description": "Шлях до файлу, який буде запущений при натисканні на кнопку \"Play\"",
|
||||
"executable_section_title": "Файл",
|
||||
"last_downloaded_option": "Останній варіант завантаження",
|
||||
"next_screenshot": "Наступний скрішнот",
|
||||
"no_executable_selected": "Файл не вибрано",
|
||||
"no_shop_details": "Не вдалося отримати опис",
|
||||
"open_download_location": "Переглянути папку завантажень",
|
||||
"open_folder": "Відкрити папку",
|
||||
"open_screenshot": "Відкрити скріншот",
|
||||
"options": "Налаштування",
|
||||
"paused": "Призупинено",
|
||||
"previous_screenshot": "Попередній скріншот",
|
||||
"remove_files": "Видалити файли",
|
||||
"remove_from_library_description": "{{game}} буде видалено з вашої бібліотеки",
|
||||
"remove_from_library_title": "Ви впевнені?",
|
||||
"screenshot": "Скріншот",
|
||||
"select_executable": "Обрати"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активувати Hydra",
|
||||
"installation_id": "ID установки:",
|
||||
"enter_activation_code": "Введіть ваш активаційний код",
|
||||
"message": "Якщо ви не знаєте, де його запросити, то не повинні мати цього.",
|
||||
"message": "Якщо ви не знаєте, де його запросити, то не повинні мати його.",
|
||||
"activate": "Активувати",
|
||||
"loading": "Завантаження…"
|
||||
},
|
||||
|
@ -107,20 +126,23 @@
|
|||
"eta": "Закінчення {{eta}}",
|
||||
"paused": "Призупинено",
|
||||
"verifying": "Перевірка…",
|
||||
"completed_at": "Завершено в {{date}}",
|
||||
"completed": "Завершено",
|
||||
"download_again": "Завантажити знову",
|
||||
"cancel": "Скасувати",
|
||||
"filter": "Фільтр завантажених ігор",
|
||||
"remove": "Видалити",
|
||||
"downloading_metadata": "Завантаження метаданих…",
|
||||
"starting_download": "Початок завантаження…",
|
||||
"deleting": "Видалення інсталятора…",
|
||||
"delete": "Видалити інсталятор",
|
||||
"remove_from_list": "Видалити",
|
||||
"delete_modal_title": "Ви впевнені?",
|
||||
"delete_modal_description": "Це видалить усі інсталяційні файли з вашого комп'ютера",
|
||||
"install": "Встановити"
|
||||
"install": "Встановити",
|
||||
"download_in_progress": "В процесі",
|
||||
"downloads_completed": "Завершено",
|
||||
"no_downloads_description": "Ви ще нічого не завантажили через Hydra, але ніколи не пізно почати.",
|
||||
"no_downloads_title": "Тут так пусто...",
|
||||
"queued": "В черзі",
|
||||
"queued_downloads": "Завантаження в черзі",
|
||||
"removed": "Не завантажено"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Тека завантажень",
|
||||
|
@ -128,11 +150,45 @@
|
|||
"notifications": "Повідомлення",
|
||||
"enable_download_notifications": "Після завершення завантаження",
|
||||
"enable_repack_list_notifications": "Коли додається новий репак",
|
||||
"telemetry": "Телеметрія",
|
||||
"telemetry_description": "Відправляти анонімну статистику використання",
|
||||
"behavior": "Поведінка",
|
||||
"quit_app_instead_hiding": "Закривати програму замість того, щоб згортати її в трей",
|
||||
"launch_with_system": "Запускати програми із запуском комп'ютера"
|
||||
"quit_app_instead_hiding": "Закривати Hydra замість того, щоб згортати її в трей",
|
||||
"launch_with_system": "Запускати Hydra із запуском комп'ютера",
|
||||
"add_download_source": "Добавити джерело",
|
||||
"add_download_source_description": "Введіть посилання на .json-файл",
|
||||
"added_download_source": "Джерело для завантаження було додано",
|
||||
"changes_saved": "Зміни успішно збережено",
|
||||
"download_count_one": "{{countFormatted}} завантаження в списку",
|
||||
"download_count_other": "{{countFormatted}} завантажень в списку",
|
||||
"download_count_zero": "В списку немає завантажень",
|
||||
"download_options_one": "{{countFormatted}} доступний варіант завантаження",
|
||||
"download_options_other": "{{countFormatted}} доступних варіантів завантаження",
|
||||
"download_options_zero": "Немає доступних завантажень",
|
||||
"download_source_errored": "Помилка",
|
||||
"download_source_up_to_date": "Оновлено",
|
||||
"download_source_url": "Посилання на джерело",
|
||||
"download_sources": "Джерела для завантаження",
|
||||
"download_sources_description": "Hydra буде отримувати посилання для завантажень із цих джерел. URL має містити пряме посилання на .json-файл із посиланнями для завантажень.",
|
||||
"download_sources_synced": "Всі джерела для завантаження синхронізовано",
|
||||
"enable_real_debrid": "Включити Real-Debrid",
|
||||
"found_download_option_one": "Знайдено {{countFormatted}} варіант завантаження",
|
||||
"found_download_option_other": "Знайдено {{countFormatted}} варіантів завантаження",
|
||||
"found_download_option_zero": "Немає доступних завантажень",
|
||||
"general": "Основні",
|
||||
"import": "Імпортувати",
|
||||
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
|
||||
"language": "Мова",
|
||||
"real_debrid_api_token": "API-токен",
|
||||
"real_debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||
"real_debrid_description": "Real-Debrid — це необмежений завантажувач, який дозволяє швидко завантажувати файли, розміщені в Інтернеті, або миттєво передавати їх у плеєр через приватну мережу, що дозволяє обходити будь-які блокування.",
|
||||
"real_debrid_free_account_error": "Акаунт \"{{username}}\" - не має наявної підписки. Будь ласка, оформіть підписку на Real-Debrid",
|
||||
"real_debrid_invalid_token": "Невірний API-токен",
|
||||
"real_debrid_linked_message": "Акаунт \"{{username}}\" привязаний",
|
||||
"remove_download_source": "Видалити",
|
||||
"removed_download_source": "Джерело завантажень було видалено",
|
||||
"save_changes": "Зберегти зміни",
|
||||
"sync_download_sources": "Синхронізувати джерела",
|
||||
"validate_download_source": "Перевірити"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Завантаження завершено",
|
||||
|
@ -155,5 +211,30 @@
|
|||
},
|
||||
"modal": {
|
||||
"close": "Закрити"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Показувати пароль"
|
||||
},
|
||||
"user_profile": {
|
||||
"activity": "Остання активність",
|
||||
"amount_hours": "{{amount}} годин",
|
||||
"amount_minutes": "{{amount}} хвилин",
|
||||
"cancel": "Скасувати",
|
||||
"display_name": "Відображуване ім'я",
|
||||
"edit_profile": "Редагувати профіль",
|
||||
"last_time_played": "Остання гра {{period}}",
|
||||
"library": "Бібліотека",
|
||||
"no_recent_activity_description": "Ви давно не грали в ігри. Пора це змінити!",
|
||||
"no_recent_activity_title": "Хммм... Тут нічого немає",
|
||||
"playing_for": "Зіграно {{amount}}",
|
||||
"save": "Збережено",
|
||||
"saved_successfully": "Успішно збережено",
|
||||
"saving": "Збереження",
|
||||
"sign_out": "Вийти",
|
||||
"sign_out_modal_text": "Ваша бібліотека пов'язана з поточним обліковим записом. При виході з системи ваша бібліотека буде недоступною, і прогрес не буде збережено. Продовжити вихід?",
|
||||
"sign_out_modal_title": "Ви впевнені?",
|
||||
"successfully_signed_out": "Успішний вихід з акаунту",
|
||||
"total_play_time": "Всього зіграно: {{amount}}",
|
||||
"try_again": "Будь ласка, попробуйте ще раз"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"app": {
|
||||
"successfully_signed_in": "已成功登录"
|
||||
},
|
||||
"home": {
|
||||
"featured": "特色推荐",
|
||||
"recently_added": "最近添加",
|
||||
"trending": "最近热门",
|
||||
"surprise_me": "向我推荐",
|
||||
"no_results": "没有找到结果"
|
||||
|
@ -15,25 +17,26 @@
|
|||
"paused": "{{title}} (已暂停)",
|
||||
"downloading": "{{title}} ({{percentage}} - 正在下载…)",
|
||||
"filter": "筛选游戏库",
|
||||
"follow_us": "关注我们",
|
||||
"home": "主页",
|
||||
"discord": "加入我们的Discord",
|
||||
"telegram": "加入我们的Telegram",
|
||||
"x": "在X上关注我们",
|
||||
"github": "在GitHub上贡献"
|
||||
"queued": "{{title}} (已加入下载队列)",
|
||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||
"sign_in": "登入"
|
||||
},
|
||||
"header": {
|
||||
"search": "搜索",
|
||||
"search": "搜索游戏",
|
||||
"home": "主页",
|
||||
"catalogue": "游戏目录",
|
||||
"downloads": "下载中心",
|
||||
"search_results": "搜索结果",
|
||||
"settings": "设置"
|
||||
"settings": "设置",
|
||||
"version_available_install": "版本 {{version}} 已可用. 点击此处重新启动并安装.",
|
||||
"version_available_download": "版本 {{version}} 可用. 点击此处下载."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "没有正在进行的下载",
|
||||
"downloading_metadata": "正在下载{{title}}的元数据…",
|
||||
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}"
|
||||
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}",
|
||||
"calculating_eta": "正在下载 {{title}}… (已完成{{percentage}}.) - 正在计算剩余时间..."
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "下一页",
|
||||
|
@ -50,7 +53,6 @@
|
|||
"pause": "暂停",
|
||||
"cancel": "取消",
|
||||
"remove": "移除",
|
||||
"remove_from_list": "从列表中移除",
|
||||
"space_left_on_disk": "磁盘剩余空间{{space}}",
|
||||
"eta": "预计完成时间{{eta}}",
|
||||
"downloading_metadata": "正在下载元数据…",
|
||||
|
@ -58,12 +60,8 @@
|
|||
"requirements": "配置要求",
|
||||
"minimum": "最低要求",
|
||||
"recommended": "推荐要求",
|
||||
"no_minimum_requirements": "{{title}}没有提供最低要求信息",
|
||||
"no_recommended_requirements": "{{title}}没有提供推荐要求信息",
|
||||
"release_date": "发布于{{date}}",
|
||||
"publisher": "发行商{{publisher}}",
|
||||
"copy_link_to_clipboard": "复制链接",
|
||||
"copied_link_to_clipboard": "链接已复制",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"amount_hours": "{{amount}}小时",
|
||||
|
@ -84,17 +82,32 @@
|
|||
"repacks_modal_description": "选择您想要下载的重打包",
|
||||
"select_folder_hint": "要更改默认文件夹,请访问",
|
||||
"download_now": "立即下载",
|
||||
"installation_instructions": "安装说明",
|
||||
"installation_instructions_description": "安装这个游戏需要额外的步骤",
|
||||
"online_fix_instruction": "OnlineFix游戏需要密码才能解压。需要时,使用以下密码:",
|
||||
"dodi_installation_instruction": "打开DODI安装程序时,按键盘上的键<0 />开始安装过程:",
|
||||
"dont_show_it_again": "不再显示",
|
||||
"copied_to_clipboard": "已复制到剪贴板",
|
||||
"got_it": "我已知晓",
|
||||
"previous_screenshot": "上一张截图",
|
||||
"next_screenshot": "下一张截图",
|
||||
"screenshot": "截图 {{number}}",
|
||||
"open_screenshot": "打开截图 {{number}}"
|
||||
"open_screenshot": "打开截图 {{number}}",
|
||||
"download_settings": "下载设置",
|
||||
"downloader": "下载器",
|
||||
"select_executable": "选择",
|
||||
"no_executable_selected": "没有可执行文件被指定",
|
||||
"open_folder": "打开目录",
|
||||
"open_download_location": "查看已下载的文件",
|
||||
"create_shortcut": "创建桌面快捷方式",
|
||||
"remove_files": "删除文件",
|
||||
"remove_from_library_title": "你确定吗?",
|
||||
"remove_from_library_description": "这将会把 {{game}} 从你的库中移除",
|
||||
"options": "选项",
|
||||
"executable_section_title": "可执行文件",
|
||||
"executable_section_description": "点击 \"Play\" 时将执行的文件的路径",
|
||||
"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": "创建快捷方式出错"
|
||||
},
|
||||
"activation": {
|
||||
"title": "激活 Hydra",
|
||||
|
@ -110,20 +123,22 @@
|
|||
"eta": "预计完成时间{{eta}}",
|
||||
"paused": "已暂停",
|
||||
"verifying": "正在验证…",
|
||||
"completed_at": "完成于{{date}}",
|
||||
"completed": "已完成",
|
||||
"download_again": "再次下载",
|
||||
"cancel": "取消",
|
||||
"filter": "筛选已下载游戏",
|
||||
"remove": "移除",
|
||||
"downloading_metadata": "正在下载元数据…",
|
||||
"starting_download": "开始下载…",
|
||||
"deleting": "正在删除安装程序…",
|
||||
"delete": "移除安装程序",
|
||||
"remove_from_list": "移除",
|
||||
"delete_modal_title": "您确定吗?",
|
||||
"delete_modal_description": "这将从您的电脑上移除所有的安装文件",
|
||||
"install": "安装"
|
||||
"install": "安装",
|
||||
"download_in_progress": "进行中",
|
||||
"queued_downloads": "在队列中的下载",
|
||||
"downloads_completed": "已完成",
|
||||
"queued": "下载列表",
|
||||
"no_downloads_title": "空空如也",
|
||||
"no_downloads_description": "你还未使用Hydra下载任何游戏,但什么时候开始,都为时不晚。"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "下载路径",
|
||||
|
@ -131,36 +146,72 @@
|
|||
"notifications": "通知",
|
||||
"enable_download_notifications": "下载完成时",
|
||||
"enable_repack_list_notifications": "添加新重打包时",
|
||||
"telemetry": "遥测",
|
||||
"telemetry_description": "启用匿名使用统计",
|
||||
"real_debrid_api_token_label": "Real-Debrid API 令牌",
|
||||
"quit_app_instead_hiding": "关闭Hydra而不是最小化到托盘",
|
||||
"launch_with_system": "系统启动时运行 Hydra",
|
||||
"general": "通用",
|
||||
"behavior": "行为",
|
||||
"general": "常规",
|
||||
"quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘",
|
||||
"launch_with_system": "随系统启动时运行应用程序",
|
||||
"download_sources": "下载源",
|
||||
"language": "语言",
|
||||
"real_debrid_api_token": "API 令牌",
|
||||
"enable_real_debrid": "启用 Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
|
||||
"real_debrid_invalid_token": "无效的 API 令牌",
|
||||
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
|
||||
"save_changes": "保存更改"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "下载完成",
|
||||
"game_ready_to_install": "{{title}}已准备好安装",
|
||||
"repack_list_updated": "重打包列表已更新",
|
||||
"repack_count_one": "已添加{{count}}个重打包",
|
||||
"repack_count_other": "已添加{{count}}个重打包"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "打开Hydra",
|
||||
"quit": "退出"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "没有可用的下载"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "程序未安装",
|
||||
"description": "在您的系统上未找到Wine或Lutris的可执行文件",
|
||||
"instructions": "检查在您的Linux发行版上正确安装它们的方法,以便游戏可以正常运行"
|
||||
"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_options_zero": "无可用下载",
|
||||
"download_options_one": "有 {{countFormatted}} 个下载可用",
|
||||
"download_options_other": "有 {{countFormatted}} 个下载可用",
|
||||
"download_source_url": "下载源 URL",
|
||||
"add_download_source_description": "插入包含 .json 文件的 URL",
|
||||
"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": "导入"
|
||||
},
|
||||
"modal": {
|
||||
"close": "关闭按钮"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "切换密码可见性"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} 小时",
|
||||
"amount_minutes": "{{amount}} 分钟",
|
||||
"last_time_played": "上次游玩时间 {{period}}",
|
||||
"activity": "近期活动",
|
||||
"library": "库",
|
||||
"total_play_time": "总游戏时长: {{amount}}",
|
||||
"no_recent_activity_title": "Emmm… 这里暂时啥都没有",
|
||||
"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": "Playing for {{amount}}",
|
||||
"sign_out_modal_text": "您的资料库与您当前的账户相关联。注销后,您的资料库将不再可见,任何进度也不会保存。继续退出吗?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@ export class Game {
|
|||
@Column("text", { unique: true })
|
||||
objectID: string;
|
||||
|
||||
@Column("text", { unique: true, nullable: true })
|
||||
remoteId: string | null;
|
||||
|
||||
@Column("text")
|
||||
title: string;
|
||||
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from "./user-preferences.entity";
|
|||
export * from "./game-shop-cache.entity";
|
||||
export * from "./download-source.entity";
|
||||
export * from "./download-queue.entity";
|
||||
export * from "./user-auth";
|
||||
|
|
|
@ -11,6 +11,15 @@ export class UserAuth {
|
|||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
userId: string;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
displayName: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
profileImageUrl: string | null;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
accessToken: string;
|
||||
|
||||
|
|
14
src/main/events/auth/get-session-hash.ts
Normal file
14
src/main/events/auth/get-session-hash.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const auth = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
|
||||
if (!auth) return null;
|
||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||
return payload.sessionId;
|
||||
};
|
||||
|
||||
registerEvent("getSessionHash", getSessionHash);
|
7
src/main/events/auth/open-auth-window.ts
Normal file
7
src/main/events/auth/open-auth-window.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const openAuthWindow = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
WindowManager.openAuthWindow();
|
||||
|
||||
registerEvent("openAuthWindow", openAuthWindow);
|
31
src/main/events/auth/sign-out.ts
Normal file
31
src/main/events/auth/sign-out.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game, UserAuth } from "@main/entity";
|
||||
|
||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const databaseOperations = dataSource
|
||||
.transaction(async (transactionalEntityManager) => {
|
||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({});
|
||||
|
||||
await transactionalEntityManager.getRepository(Game).delete({});
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(UserAuth)
|
||||
.delete({ id: 1 });
|
||||
})
|
||||
.then(() => {
|
||||
/* Removes all games being played */
|
||||
gamesPlaytime.clear();
|
||||
});
|
||||
|
||||
/* Disconnects aria2 */
|
||||
DownloadManager.disconnect();
|
||||
|
||||
await Promise.all([
|
||||
databaseOperations,
|
||||
HydraApi.post("/auth/logout").catch(),
|
||||
]);
|
||||
};
|
||||
|
||||
registerEvent("signOut", signOut);
|
|
@ -5,7 +5,6 @@ export const downloadSourceSchema = z.object({
|
|||
downloads: z.array(
|
||||
z.object({
|
||||
title: z.string().max(255),
|
||||
downloaders: z.array(z.enum(["real_debrid", "torrent"])),
|
||||
uris: z.array(z.string()),
|
||||
uploadDate: z.string().max(255),
|
||||
fileSize: z.string().max(255),
|
||||
|
|
|
@ -22,6 +22,7 @@ import "./library/open-game-installer-path";
|
|||
import "./library/update-executable-path";
|
||||
import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./misc/is-user-logged-in";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
import "./torrenting/cancel-game-download";
|
||||
|
@ -39,6 +40,12 @@ import "./download-sources/validate-download-source";
|
|||
import "./download-sources/add-download-source";
|
||||
import "./download-sources/remove-download-source";
|
||||
import "./download-sources/sync-download-sources";
|
||||
import "./auth/sign-out";
|
||||
import "./auth/open-auth-window";
|
||||
import "./auth/get-session-hash";
|
||||
import "./user/get-user";
|
||||
import "./profile/get-me";
|
||||
import "./profile/update-profile";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => app.getVersion());
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { GameShop } from "@types";
|
|||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
|
||||
const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
|
@ -49,6 +50,21 @@ const addGameToLibrary = async (
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
const game = await gameRepository.findOne({ where: { objectID } });
|
||||
|
||||
createGame(game!).then((response) => {
|
||||
const {
|
||||
id: remoteId,
|
||||
playTimeInMilliseconds,
|
||||
lastTimePlayed,
|
||||
} = response.data;
|
||||
|
||||
gameRepository.update(
|
||||
{ objectID },
|
||||
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import { gameRepository } from "@main/repository";
|
|||
import { registerEvent } from "../register-event";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import createDesktopShortcut from "create-desktop-shortcuts";
|
||||
import path from "node:path";
|
||||
import { app } from "electron";
|
||||
|
||||
const createGameShortcut = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
|
@ -14,10 +16,17 @@ const createGameShortcut = async (
|
|||
if (game) {
|
||||
const filePath = game.executablePath;
|
||||
|
||||
const options = { filePath, name: game.title };
|
||||
const windowVbsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "windows.vbs")
|
||||
: undefined;
|
||||
|
||||
const options = {
|
||||
filePath,
|
||||
name: game.title,
|
||||
};
|
||||
|
||||
return createDesktopShortcut({
|
||||
windows: options,
|
||||
windows: { ...options, VBScriptPath: windowVbsPath },
|
||||
linux: options,
|
||||
osx: options,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
|
||||
const removeGameFromLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
|
@ -9,6 +10,18 @@ const removeGameFromLibrary = async (
|
|||
{ id: gameId },
|
||||
{ isDeleted: true, executablePath: null }
|
||||
);
|
||||
|
||||
removeRemoveGameFromLibrary(gameId).catch((err) => {
|
||||
logger.error("removeRemoveGameFromLibrary", err);
|
||||
});
|
||||
};
|
||||
|
||||
const removeRemoveGameFromLibrary = async (gameId: number) => {
|
||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||
|
||||
if (game?.remoteId) {
|
||||
HydraApi.delete(`/games/${game.remoteId}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("removeGameFromLibrary", removeGameFromLibrary);
|
||||
|
|
8
src/main/events/misc/is-user-logged-in.ts
Normal file
8
src/main/events/misc/is-user-logged-in.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const isUserLoggedIn = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.isLoggedIn();
|
||||
};
|
||||
|
||||
registerEvent("isUserLoggedIn", isUserLoggedIn);
|
32
src/main/events/profile/get-me.ts
Normal file
32
src/main/events/profile/get-me.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { UserProfile } from "@types";
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
const getMe = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
): Promise<UserProfile | null> => {
|
||||
return HydraApi.get(`/profile/me`)
|
||||
.then((response) => {
|
||||
const me = response.data;
|
||||
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
displayName: me.displayName,
|
||||
profileImageUrl: me.profileImageUrl,
|
||||
userId: me.id,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
return me;
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("getMe", err);
|
||||
return userAuthRepository.findOne({ where: { id: 1 } });
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("getMe", getMe);
|
61
src/main/events/profile/update-profile.ts
Normal file
61
src/main/events/profile/update-profile.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import axios from "axios";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileTypeFromFile } from "file-type";
|
||||
import { UserProfile } from "@types";
|
||||
|
||||
const patchUserProfile = async (
|
||||
displayName: string,
|
||||
profileImageUrl?: string
|
||||
) => {
|
||||
if (profileImageUrl) {
|
||||
return HydraApi.patch("/profile", {
|
||||
displayName,
|
||||
profileImageUrl,
|
||||
});
|
||||
} else {
|
||||
return HydraApi.patch("/profile", {
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateProfile = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
displayName: string,
|
||||
newProfileImagePath: string | null
|
||||
): Promise<UserProfile> => {
|
||||
if (!newProfileImagePath) {
|
||||
return (await patchUserProfile(displayName)).data;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(newProfileImagePath);
|
||||
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
||||
const fileSizeInBytes = stats.size;
|
||||
|
||||
const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, {
|
||||
imageExt: path.extname(newProfileImagePath).slice(1),
|
||||
imageLength: fileSizeInBytes,
|
||||
})
|
||||
.then(async (preSignedResponse) => {
|
||||
const { presignedUrl, profileImageUrl } = preSignedResponse.data;
|
||||
|
||||
const mimeType = await fileTypeFromFile(newProfileImagePath);
|
||||
|
||||
await axios.put(presignedUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": mimeType?.mime,
|
||||
},
|
||||
});
|
||||
return profileImageUrl;
|
||||
})
|
||||
.catch(() => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return (await patchUserProfile(displayName, profileImageUrl)).data;
|
||||
};
|
||||
|
||||
registerEvent("updateProfile", updateProfile);
|
|
@ -12,6 +12,7 @@ import { DownloadManager } from "@main/services";
|
|||
|
||||
import { Not } from "typeorm";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
|
@ -94,6 +95,19 @@ const startGameDownload = async (
|
|||
},
|
||||
});
|
||||
|
||||
createGame(updatedGame!).then((response) => {
|
||||
const {
|
||||
id: remoteId,
|
||||
playTimeInMilliseconds,
|
||||
lastTimePlayed,
|
||||
} = response.data;
|
||||
|
||||
gameRepository.update(
|
||||
{ objectID },
|
||||
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
|
||||
);
|
||||
});
|
||||
|
||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||
|
||||
|
|
56
src/main/events/user/get-user.ts
Normal file
56
src/main/events/user/get-user.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { UserProfile } from "@types";
|
||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
|
||||
const getUser = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string
|
||||
): Promise<UserProfile | null> => {
|
||||
try {
|
||||
const response = await HydraApi.get(`/user/${userId}`);
|
||||
const profile = response.data;
|
||||
|
||||
const recentGames = await Promise.all(
|
||||
profile.recentGames.map(async (game) => {
|
||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...game,
|
||||
...convertSteamGameToCatalogueEntry(steamGame),
|
||||
iconUrl,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const libraryGames = await Promise.all(
|
||||
profile.libraryGames.map(async (game) => {
|
||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...game,
|
||||
...convertSteamGameToCatalogueEntry(steamGame),
|
||||
iconUrl,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return { ...profile, libraryGames, recentGames };
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("getUser", getUser);
|
|
@ -2,6 +2,7 @@ import { app, BrowserWindow, net, protocol } from "electron";
|
|||
import updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { DownloadManager, logger, WindowManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
|
@ -50,9 +51,10 @@ if (process.defaultApp) {
|
|||
app.whenReady().then(async () => {
|
||||
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
||||
|
||||
protocol.handle("hydra", (request) =>
|
||||
net.fetch("file://" + request.url.slice("hydra://".length))
|
||||
);
|
||||
protocol.handle("local", (request) => {
|
||||
const filePath = request.url.slice("local:".length);
|
||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
await dataSource.runMigrations();
|
||||
|
@ -71,6 +73,15 @@ app.on("browser-window-created", (_, window) => {
|
|||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
const handleDeepLinkPath = (uri?: string) => {
|
||||
if (!uri) return;
|
||||
const url = new URL(uri);
|
||||
|
||||
if (url.host === "install-source") {
|
||||
WindowManager.redirect(`settings${url.search}`);
|
||||
}
|
||||
};
|
||||
|
||||
app.on("second-instance", (_event, commandLine) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (WindowManager.mainWindow) {
|
||||
|
@ -82,13 +93,11 @@ app.on("second-instance", (_event, commandLine) => {
|
|||
WindowManager.createMainWindow();
|
||||
}
|
||||
|
||||
const [, path] = commandLine.pop()?.split("://") ?? [];
|
||||
if (path) WindowManager.redirect(path);
|
||||
handleDeepLinkPath(commandLine.pop());
|
||||
});
|
||||
|
||||
app.on("open-url", (_event, url) => {
|
||||
const [, path] = url.split("://");
|
||||
WindowManager.redirect(path);
|
||||
handleDeepLinkPath(url);
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
|
|
|
@ -10,6 +10,7 @@ import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
|||
import { publishNewRepacksNotifications } from "./services/notifications";
|
||||
import { MoreThan } from "typeorm";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
|
||||
startMainLoop();
|
||||
|
||||
|
@ -21,7 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||
if (userPreferences?.realDebridApiToken)
|
||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||
|
||||
HydraApi.setupApi();
|
||||
HydraApi.setupApi().then(async () => {
|
||||
if (HydraApi.isLoggedIn()) uploadGamesBatch();
|
||||
});
|
||||
|
||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||
order: {
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
GameShopCache,
|
||||
Repack,
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
} from "@main/entity";
|
||||
import { UserAuth } from "./entity/user-auth";
|
||||
|
||||
export const gameRepository = dataSource.getRepository(Game);
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ export class DownloadManager {
|
|||
public static disconnect() {
|
||||
if (this.aria2c) {
|
||||
this.aria2c.kill();
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,100 @@
|
|||
import { userAuthRepository } from "@main/repository";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import url from "url";
|
||||
import { uploadGamesBatch } from "./library-sync";
|
||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export class HydraApi {
|
||||
private static instance: AxiosInstance;
|
||||
|
||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5;
|
||||
|
||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
||||
|
||||
private static userAuth = {
|
||||
authToken: "",
|
||||
refreshToken: "",
|
||||
expirationTimestamp: 0,
|
||||
};
|
||||
|
||||
static isLoggedIn() {
|
||||
return this.userAuth.authToken !== "";
|
||||
}
|
||||
|
||||
static async handleExternalAuth(uri: string) {
|
||||
const { payload } = url.parse(uri, true).query;
|
||||
|
||||
const decodedBase64 = atob(payload as string);
|
||||
const jsonData = JSON.parse(decodedBase64);
|
||||
|
||||
const { accessToken, expiresIn, refreshToken } = jsonData;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const tokenExpirationTimestamp =
|
||||
now.getTime() +
|
||||
this.secondsToMilliseconds(expiresIn) -
|
||||
this.EXPIRATION_OFFSET_IN_MS;
|
||||
|
||||
this.userAuth = {
|
||||
authToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
expirationTimestamp: tokenExpirationTimestamp,
|
||||
};
|
||||
|
||||
await userAuthRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
accessToken,
|
||||
tokenExpirationTimestamp,
|
||||
refreshToken,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-signin");
|
||||
await clearGamesRemoteIds();
|
||||
uploadGamesBatch();
|
||||
}
|
||||
}
|
||||
|
||||
static async setupApi() {
|
||||
this.instance = axios.create({
|
||||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||
});
|
||||
|
||||
this.instance.interceptors.request.use(
|
||||
(request) => {
|
||||
logger.log(" ---- REQUEST -----");
|
||||
logger.log(request.method, request.url, request.data);
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
logger.log("request error", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
logger.log(" ---- RESPONSE -----");
|
||||
logger.log(
|
||||
response.status,
|
||||
response.config.method,
|
||||
response.config.url,
|
||||
response.data
|
||||
);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
logger.error("response error", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const userAuth = await userAuthRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
@ -29,28 +107,59 @@ export class HydraApi {
|
|||
}
|
||||
|
||||
private static async revalidateAccessTokenIfExpired() {
|
||||
if (!this.userAuth.authToken) {
|
||||
userAuthRepository.delete({ id: 1 });
|
||||
logger.error("user is not logged in");
|
||||
throw new Error("user is not logged in");
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
if (this.userAuth.expirationTimestamp < now.getTime()) {
|
||||
const response = await this.instance.post(`/auth/refresh`, {
|
||||
refreshToken: this.userAuth.refreshToken,
|
||||
});
|
||||
try {
|
||||
const response = await this.instance.post(`/auth/refresh`, {
|
||||
refreshToken: this.userAuth.refreshToken,
|
||||
});
|
||||
|
||||
const { accessToken, expiresIn } = response.data;
|
||||
const { accessToken, expiresIn } = response.data;
|
||||
|
||||
const tokenExpirationTimestamp =
|
||||
now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS;
|
||||
const tokenExpirationTimestamp =
|
||||
now.getTime() +
|
||||
this.secondsToMilliseconds(expiresIn) -
|
||||
this.EXPIRATION_OFFSET_IN_MS;
|
||||
|
||||
this.userAuth.authToken = accessToken;
|
||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||
this.userAuth.authToken = accessToken;
|
||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
accessToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
accessToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof AxiosError &&
|
||||
(err?.response?.status === 401 || err?.response?.status === 403)
|
||||
) {
|
||||
this.userAuth = {
|
||||
authToken: "",
|
||||
expirationTimestamp: 0,
|
||||
refreshToken: "",
|
||||
};
|
||||
|
||||
userAuthRepository.delete({ id: 1 });
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-signout");
|
||||
}
|
||||
|
||||
logger.log("user refresh token expired");
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,13 +181,18 @@ export class HydraApi {
|
|||
return this.instance.post(url, data, this.getAxiosConfig());
|
||||
}
|
||||
|
||||
static async put(url, data?: any) {
|
||||
static async put(url: string, data?: any) {
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance.put(url, data, this.getAxiosConfig());
|
||||
}
|
||||
|
||||
static async patch(url, data?: any) {
|
||||
static async patch(url: string, data?: any) {
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance.patch(url, data, this.getAxiosConfig());
|
||||
}
|
||||
|
||||
static async delete(url: string) {
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance.delete(url, this.getAxiosConfig());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,3 +8,4 @@ export * from "./how-long-to-beat";
|
|||
export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
export * from "./repacks-manager";
|
||||
export * from "./hydra-api";
|
||||
|
|
5
src/main/services/library-sync/clear-games-remote-id.ts
Normal file
5
src/main/services/library-sync/clear-games-remote-id.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { gameRepository } from "@main/repository";
|
||||
|
||||
export const clearGamesRemoteIds = () => {
|
||||
return gameRepository.update({}, { remoteId: null });
|
||||
};
|
11
src/main/services/library-sync/create-game.ts
Normal file
11
src/main/services/library-sync/create-game.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Game } from "@main/entity";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
export const createGame = async (game: Game) => {
|
||||
return HydraApi.post(`/games`, {
|
||||
objectId: game.objectID,
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
shop: game.shop,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
});
|
||||
};
|
4
src/main/services/library-sync/index.ts
Normal file
4
src/main/services/library-sync/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from "./merge-with-remote-games";
|
||||
export * from "./upload-games-batch";
|
||||
export * from "./update-game-playtime";
|
||||
export * from "./create-game";
|
72
src/main/services/library-sync/merge-with-remote-games.ts
Normal file
72
src/main/services/library-sync/merge-with-remote-games.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { gameRepository } from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import { logger } from "../logger";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
export const mergeWithRemoteGames = async () => {
|
||||
try {
|
||||
const games = await HydraApi.get("/games");
|
||||
|
||||
for (const game of games.data) {
|
||||
const localGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID: game.objectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (localGame) {
|
||||
const updatedLastTimePlayed =
|
||||
localGame.lastTimePlayed == null ||
|
||||
(game.lastTimePlayed &&
|
||||
new Date(game.lastTimePlayed) > localGame.lastTimePlayed)
|
||||
? game.lastTimePlayed
|
||||
: localGame.lastTimePlayed;
|
||||
|
||||
const updatedPlayTime =
|
||||
localGame.playTimeInMilliseconds < game.playTimeInMilliseconds
|
||||
? game.playTimeInMilliseconds
|
||||
: localGame.playTimeInMilliseconds;
|
||||
|
||||
gameRepository.update(
|
||||
{
|
||||
objectID: game.objectId,
|
||||
shop: "steam",
|
||||
},
|
||||
{
|
||||
remoteId: game.id,
|
||||
lastTimePlayed: updatedLastTimePlayed,
|
||||
playTimeInMilliseconds: updatedPlayTime,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
if (steamGame) {
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
gameRepository.insert({
|
||||
objectID: game.objectId,
|
||||
title: steamGame?.name,
|
||||
remoteId: game.id,
|
||||
shop: game.shop,
|
||||
iconUrl,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError) {
|
||||
logger.error("getRemoteGames", err.response, err.message);
|
||||
} else {
|
||||
logger.error("getRemoteGames", err);
|
||||
}
|
||||
}
|
||||
};
|
13
src/main/services/library-sync/update-game-playtime.ts
Normal file
13
src/main/services/library-sync/update-game-playtime.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Game } from "@main/entity";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
export const updateGamePlaytime = async (
|
||||
game: Game,
|
||||
deltaInMillis: number,
|
||||
lastTimePlayed: Date
|
||||
) => {
|
||||
return HydraApi.put(`/games/${game.remoteId}`, {
|
||||
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
|
||||
lastTimePlayed,
|
||||
});
|
||||
};
|
44
src/main/services/library-sync/upload-games-batch.ts
Normal file
44
src/main/services/library-sync/upload-games-batch.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { gameRepository } from "@main/repository";
|
||||
import { chunk } from "lodash-es";
|
||||
import { IsNull } from "typeorm";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { logger } from "../logger";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { mergeWithRemoteGames } from "./merge-with-remote-games";
|
||||
import { WindowManager } from "../window-manager";
|
||||
|
||||
export const uploadGamesBatch = async () => {
|
||||
try {
|
||||
const games = await gameRepository.find({
|
||||
where: { remoteId: IsNull(), isDeleted: false },
|
||||
});
|
||||
|
||||
const gamesChunks = chunk(games, 200);
|
||||
|
||||
for (const chunk of gamesChunks) {
|
||||
await HydraApi.post(
|
||||
"/games/batch",
|
||||
chunk.map((game) => {
|
||||
return {
|
||||
objectId: game.objectID,
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
shop: game.shop,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await mergeWithRemoteGames();
|
||||
|
||||
if (WindowManager.mainWindow)
|
||||
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError) {
|
||||
logger.error("uploadGamesBatch", err.response, err.message);
|
||||
} else {
|
||||
logger.error("uploadGamesBatch", err);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -4,8 +4,13 @@ import { IsNull, Not } from "typeorm";
|
|||
import { gameRepository } from "@main/repository";
|
||||
import { getProcesses } from "@main/helpers";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { createGame, updateGamePlaytime } from "./library-sync";
|
||||
import { GameRunning } from "@types";
|
||||
|
||||
const gamesPlaytime = new Map<number, number>();
|
||||
export const gamesPlaytime = new Map<
|
||||
number,
|
||||
{ lastTick: number; firstTick: number }
|
||||
>();
|
||||
|
||||
export const watchProcesses = async () => {
|
||||
const games = await gameRepository.find({
|
||||
|
@ -37,26 +42,67 @@ export const watchProcesses = async () => {
|
|||
|
||||
if (gameProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
const zero = gamesPlaytime.get(game.id) ?? 0;
|
||||
const delta = performance.now() - zero;
|
||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
|
||||
}
|
||||
const zero = gamePlaytime.lastTick;
|
||||
const delta = performance.now() - zero;
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
gamesPlaytime.set(game.id, performance.now());
|
||||
gamesPlaytime.set(game.id, {
|
||||
...gamePlaytime,
|
||||
lastTick: performance.now(),
|
||||
});
|
||||
} else {
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(game, 0, new Date());
|
||||
} else {
|
||||
createGame({ ...game, lastTimePlayed: new Date() }).then(
|
||||
(response) => {
|
||||
const { id: remoteId } = response.data;
|
||||
gameRepository.update({ objectID: game.objectID }, { remoteId });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
gamesPlaytime.set(game.id, {
|
||||
lastTick: performance.now(),
|
||||
firstTick: performance.now(),
|
||||
});
|
||||
}
|
||||
} else if (gamesPlaytime.has(game.id)) {
|
||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||
gamesPlaytime.delete(game.id);
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
performance.now() - gamePlaytime.firstTick,
|
||||
game.lastTimePlayed!
|
||||
);
|
||||
} else {
|
||||
createGame(game).then((response) => {
|
||||
const { id: remoteId } = response.data;
|
||||
gameRepository.update({ objectID: game.objectID }, { remoteId });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
|
||||
return {
|
||||
id: entry[0],
|
||||
sessionDurationInMillis: performance.now() - entry[1].firstTick,
|
||||
};
|
||||
});
|
||||
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-games-running",
|
||||
gamesRunning as Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,12 +9,13 @@ import {
|
|||
shell,
|
||||
} from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import { t } from "i18next";
|
||||
import i18next, { t } from "i18next";
|
||||
import path from "node:path";
|
||||
import icon from "@resources/icon.png?asset";
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
|
@ -80,6 +81,48 @@ export class WindowManager {
|
|||
});
|
||||
}
|
||||
|
||||
public static openAuthWindow() {
|
||||
if (this.mainWindow) {
|
||||
const authWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 640,
|
||||
backgroundColor: "#1c1c1c",
|
||||
parent: this.mainWindow,
|
||||
modal: true,
|
||||
show: false,
|
||||
maximizable: false,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
webPreferences: {
|
||||
sandbox: false,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
},
|
||||
});
|
||||
|
||||
authWindow.removeMenu();
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
lng: i18next.language,
|
||||
});
|
||||
|
||||
authWindow.loadURL(
|
||||
`https://auth.hydra.losbroxas.org/?${searchParams.toString()}`
|
||||
);
|
||||
|
||||
authWindow.once("ready-to-show", () => {
|
||||
authWindow.show();
|
||||
});
|
||||
|
||||
authWindow.webContents.on("will-navigate", (_event, url) => {
|
||||
if (url.startsWith("hydralauncher://auth")) {
|
||||
authWindow.close();
|
||||
|
||||
HydraApi.handleExternalAuth(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static redirect(hash: string) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.loadURL(hash);
|
||||
|
|
|
@ -11,6 +11,7 @@ export const steamGamesWorker = new Piscina({
|
|||
workerData: {
|
||||
steamGamesPath: path.join(seedsPath, "steam-games.json"),
|
||||
},
|
||||
maxThreads: 1,
|
||||
});
|
||||
|
||||
export const downloadSourceWorker = new Piscina({
|
||||
|
|
|
@ -8,6 +8,7 @@ import type {
|
|||
UserPreferences,
|
||||
AppUpdaterEvent,
|
||||
StartGameDownloadPayload,
|
||||
GameRunning,
|
||||
} from "@types";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
|
@ -84,17 +85,21 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
ipcRenderer.invoke("deleteGameFolder", gameId),
|
||||
getGameByObjectID: (objectID: string) =>
|
||||
ipcRenderer.invoke("getGameByObjectID", objectID),
|
||||
onPlaytime: (cb: (gameId: number) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, gameId: number) =>
|
||||
cb(gameId);
|
||||
ipcRenderer.on("on-playtime", listener);
|
||||
return () => ipcRenderer.removeListener("on-playtime", listener);
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
) => void
|
||||
) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, gamesRunning) =>
|
||||
cb(gamesRunning);
|
||||
ipcRenderer.on("on-games-running", listener);
|
||||
return () => ipcRenderer.removeListener("on-games-running", listener);
|
||||
},
|
||||
onGameClose: (cb: (gameId: number) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, gameId: number) =>
|
||||
cb(gameId);
|
||||
ipcRenderer.on("on-game-close", listener);
|
||||
return () => ipcRenderer.removeListener("on-game-close", listener);
|
||||
onLibraryBatchComplete: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-library-batch-complete", listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
||||
},
|
||||
|
||||
/* Hardware */
|
||||
|
@ -106,6 +111,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
getVersion: () => ipcRenderer.invoke("getVersion"),
|
||||
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
||||
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
|
||||
isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"),
|
||||
showOpenDialog: (options: Electron.OpenDialogOptions) =>
|
||||
ipcRenderer.invoke("showOpenDialog", options),
|
||||
platform: process.platform,
|
||||
|
@ -125,4 +131,27 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
},
|
||||
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
|
||||
restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"),
|
||||
|
||||
/* Profile */
|
||||
getMe: () => ipcRenderer.invoke("getMe"),
|
||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
||||
|
||||
/* User */
|
||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||
|
||||
/* Auth */
|
||||
signOut: () => ipcRenderer.invoke("signOut"),
|
||||
openAuthWindow: () => ipcRenderer.invoke("openAuthWindow"),
|
||||
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
|
||||
onSignIn: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-signin", listener);
|
||||
return () => ipcRenderer.removeListener("on-signin", listener);
|
||||
},
|
||||
onSignOut: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-signout", listener);
|
||||
return () => ipcRenderer.removeListener("on-signout", listener);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<title>Hydra</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
||||
/>
|
||||
</head>
|
||||
<body style="background-color: #1c1c1c">
|
||||
|
|
|
@ -111,6 +111,6 @@ export const titleBar = style({
|
|||
alignItems: "center",
|
||||
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||
WebkitAppRegion: "drag",
|
||||
zIndex: "2",
|
||||
zIndex: "4",
|
||||
borderBottom: `1px solid ${vars.color.border}`,
|
||||
} as ComplexStyleRule);
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
useAppSelector,
|
||||
useDownload,
|
||||
useLibrary,
|
||||
useToast,
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
|
||||
import * as styles from "./app.css";
|
||||
|
@ -18,7 +20,11 @@ import {
|
|||
setUserPreferences,
|
||||
toggleDraggingDisabled,
|
||||
closeToast,
|
||||
setUserDetails,
|
||||
setProfileBackground,
|
||||
setGameRunning,
|
||||
} from "@renderer/features";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -26,21 +32,30 @@ export interface AppProps {
|
|||
|
||||
export function App() {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const { updateLibrary } = useLibrary();
|
||||
const { updateLibrary, library } = useLibrary();
|
||||
|
||||
const { t } = useTranslation("app");
|
||||
|
||||
const { clearDownload, setLastPacket } = useDownload();
|
||||
|
||||
const { fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||
useUserDetails();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const search = useAppSelector((state) => state.search.value);
|
||||
|
||||
const draggingDisabled = useAppSelector(
|
||||
(state) => state.window.draggingDisabled
|
||||
);
|
||||
|
||||
const toast = useAppSelector((state) => state.toast);
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
|
||||
([preferences]) => {
|
||||
|
@ -67,6 +82,75 @@ export function App() {
|
|||
};
|
||||
}, [clearDownload, setLastPacket, updateLibrary]);
|
||||
|
||||
useEffect(() => {
|
||||
const cachedUserDetails = window.localStorage.getItem("userDetails");
|
||||
|
||||
if (cachedUserDetails) {
|
||||
const { profileBackground, ...userDetails } =
|
||||
JSON.parse(cachedUserDetails);
|
||||
|
||||
dispatch(setUserDetails(userDetails));
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
}
|
||||
|
||||
window.electron.isUserLoggedIn().then((isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) updateUserDetails(response);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
||||
|
||||
const onSignIn = useCallback(() => {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
showSuccessToast(t("successfully_signed_in"));
|
||||
}
|
||||
});
|
||||
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
||||
if (gamesRunning.length) {
|
||||
const lastGame = gamesRunning[gamesRunning.length - 1];
|
||||
const libraryGame = library.find(
|
||||
(library) => library.id === lastGame.id
|
||||
);
|
||||
|
||||
if (libraryGame) {
|
||||
dispatch(
|
||||
setGameRunning({
|
||||
...libraryGame,
|
||||
sessionDurationInMillis: lastGame.sessionDurationInMillis,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispatch(setGameRunning(null));
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [dispatch, library]);
|
||||
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
window.electron.onSignIn(onSignIn),
|
||||
window.electron.onLibraryBatchComplete(() => {
|
||||
updateLibrary();
|
||||
}),
|
||||
window.electron.onSignOut(() => clearUserDetails()),
|
||||
];
|
||||
|
||||
return () => {
|
||||
listeners.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [onSignIn, updateLibrary, clearUserDetails]);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(query: string) => {
|
||||
dispatch(setSearch(query));
|
||||
|
@ -119,6 +203,13 @@ export function App() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Toast
|
||||
visible={toast.visible}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={handleToastClose}
|
||||
/>
|
||||
|
||||
<main>
|
||||
<Sidebar />
|
||||
|
||||
|
@ -136,13 +227,6 @@ export function App() {
|
|||
</main>
|
||||
|
||||
<BottomPanel />
|
||||
|
||||
<Toast
|
||||
visible={toast.visible}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={handleToastClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { keyframes } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const backdropFadeIn = keyframes({
|
||||
"0%": { backdropFilter: "blur(0px)", backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
||||
|
@ -30,8 +30,8 @@ export const backdrop = recipe({
|
|||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 1,
|
||||
top: 0,
|
||||
zIndex: vars.zIndex.backdrop,
|
||||
top: "0",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
backdropFilter: "blur(2px)",
|
||||
transition: "all ease 0.2s",
|
||||
|
|
|
@ -12,7 +12,7 @@ export const bottomPanel = style({
|
|||
transition: "all ease 0.2s",
|
||||
justifyContent: "space-between",
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
zIndex: vars.zIndex.bottomPanel,
|
||||
});
|
||||
|
||||
export const downloadsButton = style({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import { useDownload, useUserDetails } from "@renderer/hooks";
|
||||
|
||||
import * as styles from "./bottom-panel.css";
|
||||
|
||||
|
@ -13,16 +13,23 @@ export function BottomPanel() {
|
|||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
|
||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||
|
||||
const isGameDownloading = !!lastPacket?.game;
|
||||
|
||||
const [version, setVersion] = useState("");
|
||||
const [sessionHash, setSessionHash] = useState<null | string>("");
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getVersion().then((result) => setVersion(result));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getSessionHash().then((result) => setSessionHash(result));
|
||||
}, [userDetails?.id]);
|
||||
|
||||
const status = useMemo(() => {
|
||||
if (isGameDownloading) {
|
||||
if (lastPacket?.isDownloadingMetadata)
|
||||
|
@ -65,7 +72,8 @@ export function BottomPanel() {
|
|||
</button>
|
||||
|
||||
<small>
|
||||
v{version} "{VERSION_CODENAME}"
|
||||
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
||||
{VERSION_CODENAME}"
|
||||
</small>
|
||||
</footer>
|
||||
);
|
||||
|
|
|
@ -39,6 +39,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||
|
||||
const title = useMemo(() => {
|
||||
if (location.pathname.startsWith("/game")) return headerTitle;
|
||||
if (location.pathname.startsWith("/user")) return headerTitle;
|
||||
if (location.pathname.startsWith("/search")) return t("search_results");
|
||||
|
||||
return t(pathTitle[location.pathname]);
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
} from "@renderer/helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const FEATURED_GAME_TITLE = "Horizon Forbidden West™ Complete Edition";
|
||||
const FEATURED_GAME_ID = "2420110";
|
||||
const FEATURED_GAME_TITLE = "ELDEN RING";
|
||||
const FEATURED_GAME_ID = "1245620";
|
||||
|
||||
export function Hero() {
|
||||
const [featuredGameDetails, setFeaturedGameDetails] =
|
||||
|
@ -54,7 +54,7 @@ export function Hero() {
|
|||
>
|
||||
<div className={styles.backdrop}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(FEATURED_GAME_ID)}
|
||||
src="https://cdn2.steamgriddb.com/hero/95eb39b541856d43649b208b65b6ca9f.jpg"
|
||||
alt={FEATURED_GAME_TITLE}
|
||||
className={styles.heroMedia}
|
||||
/>
|
||||
|
|
66
src/renderer/src/components/sidebar/sidebar-profile.css.ts
Normal file
66
src/renderer/src/components/sidebar/sidebar-profile.css.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const profileButton = style({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.1s",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
color: vars.color.muted,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const profileButtonContent = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
height: "40px",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const profileButtonInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
flex: "1",
|
||||
minWidth: 0,
|
||||
});
|
||||
|
||||
export const statusBadge = style({
|
||||
width: "9px",
|
||||
height: "9px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: vars.color.danger,
|
||||
position: "absolute",
|
||||
bottom: "-2px",
|
||||
right: "-3px",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const profileButtonTitle = style({
|
||||
fontWeight: "bold",
|
||||
fontSize: vars.size.body,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
75
src/renderer/src/components/sidebar/sidebar-profile.tsx
Normal file
75
src/renderer/src/components/sidebar/sidebar-profile.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { PersonIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function SidebarProfile() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation("sidebar");
|
||||
|
||||
const { userDetails, profileBackground } = useUserDetails();
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (userDetails === null) {
|
||||
window.electron.openAuthWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/user/${userDetails!.id}`);
|
||||
};
|
||||
|
||||
const profileButtonBackground = useMemo(() => {
|
||||
if (profileBackground) return profileBackground;
|
||||
return undefined;
|
||||
}, [profileBackground]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileButton}
|
||||
style={{ background: profileButtonBackground }}
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className={styles.profileButtonContent}>
|
||||
<div className={styles.profileAvatar}>
|
||||
{userDetails?.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
src={userDetails.profileImageUrl}
|
||||
alt={userDetails.displayName}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.profileButtonInformation}>
|
||||
<p className={styles.profileButtonTitle}>
|
||||
{userDetails ? userDetails.displayName : t("sign_in")}
|
||||
</p>
|
||||
|
||||
{userDetails && gameRunning && (
|
||||
<div>
|
||||
<small>{gameRunning.title}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userDetails && gameRunning && (
|
||||
<img
|
||||
alt={gameRunning.title}
|
||||
width={24}
|
||||
style={{ borderRadius: 4 }}
|
||||
src={gameRunning.iconUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -125,46 +125,3 @@ export const section = style({
|
|||
flexDirection: "column",
|
||||
paddingBottom: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const profileButton = style({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.1s",
|
||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
alignItems: "center",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
color: vars.color.muted,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const profileButtonInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
});
|
||||
|
||||
export const statusBadge = style({
|
||||
width: "9px",
|
||||
height: "9px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: vars.color.danger,
|
||||
position: "absolute",
|
||||
bottom: "-2px",
|
||||
right: "-3px",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import * as styles from "./sidebar.css";
|
|||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { PersonIcon } from "@primer/octicons-react";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
|
||||
const SIDEBAR_MIN_WIDTH = 200;
|
||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||
|
@ -154,18 +154,7 @@ export function Sidebar() {
|
|||
maxWidth: sidebarWidth,
|
||||
}}
|
||||
>
|
||||
<button type="button" className={styles.profileButton}>
|
||||
<div className={styles.profileAvatar}>
|
||||
<PersonIcon />
|
||||
|
||||
<div className={styles.statusBadge} />
|
||||
</div>
|
||||
|
||||
<div className={styles.profileButtonInformation}>
|
||||
<p style={{ fontWeight: "bold" }}>hydra</p>
|
||||
<p style={{ fontSize: 12 }}>Jogando ABC</p>
|
||||
</div>
|
||||
</button>
|
||||
<SidebarProfile />
|
||||
|
||||
<div
|
||||
className={styles.content({
|
||||
|
|
|
@ -31,7 +31,7 @@ export const toast = recipe({
|
|||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
zIndex: "0",
|
||||
zIndex: vars.zIndex.toast,
|
||||
maxWidth: "500px",
|
||||
},
|
||||
variants: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Downloader } from "@shared";
|
||||
|
||||
export const VERSION_CODENAME = "Exodus";
|
||||
export const VERSION_CODENAME = "Leviticus";
|
||||
|
||||
export const DOWNLOADER_NAME = {
|
||||
[Downloader.RealDebrid]: "Real-Debrid",
|
||||
|
|
|
@ -109,20 +109,19 @@ export function GameDetailsContextProvider({
|
|||
}, [objectID, gameTitle, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
window.electron.onGameClose(() => {
|
||||
if (isGameRunning) setisGameRunning(false);
|
||||
}),
|
||||
window.electron.onPlaytime((gameId) => {
|
||||
if (gameId === game?.id) {
|
||||
if (!isGameRunning) setisGameRunning(true);
|
||||
updateGame();
|
||||
}
|
||||
}),
|
||||
];
|
||||
const unsubscribe = window.electron.onGamesRunning((gamesIds) => {
|
||||
const updatedIsGameRunning =
|
||||
!!game?.id &&
|
||||
!!gamesIds.find((gameRunning) => gameRunning.id == game.id);
|
||||
|
||||
if (isGameRunning != updatedIsGameRunning) {
|
||||
updateGame();
|
||||
}
|
||||
|
||||
setisGameRunning(updatedIsGameRunning);
|
||||
});
|
||||
return () => {
|
||||
listeners.forEach((unsubscribe) => unsubscribe());
|
||||
unsubscribe();
|
||||
};
|
||||
}, [game?.id, isGameRunning, updateGame]);
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from "./game-details/game-details.context";
|
||||
export * from "./settings/settings.context";
|
||||
|
|
73
src/renderer/src/context/settings/settings.context.tsx
Normal file
73
src/renderer/src/context/settings/settings.context.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { createContext, useEffect, useState } from "react";
|
||||
|
||||
import { setUserPreferences } from "@renderer/features";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import type { UserPreferences } from "@types";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export interface SettingsContext {
|
||||
updateUserPreferences: (values: Partial<UserPreferences>) => Promise<void>;
|
||||
setCurrentCategoryIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
clearSourceUrl: () => void;
|
||||
sourceUrl: string | null;
|
||||
currentCategoryIndex: number;
|
||||
}
|
||||
|
||||
export const settingsContext = createContext<SettingsContext>({
|
||||
updateUserPreferences: async () => {},
|
||||
setCurrentCategoryIndex: () => {},
|
||||
clearSourceUrl: () => {},
|
||||
sourceUrl: null,
|
||||
currentCategoryIndex: 0,
|
||||
});
|
||||
|
||||
const { Provider } = settingsContext;
|
||||
export const { Consumer: SettingsContextConsumer } = settingsContext;
|
||||
|
||||
export interface SettingsContextProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SettingsContextProvider({
|
||||
children,
|
||||
}: SettingsContextProviderProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [sourceUrl, setSourceUrl] = useState<string | null>(null);
|
||||
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const defaultSourceUrl = searchParams.get("urls");
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceUrl) setCurrentCategoryIndex(2);
|
||||
}, [sourceUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultSourceUrl) {
|
||||
setSourceUrl(defaultSourceUrl);
|
||||
}
|
||||
}, [defaultSourceUrl]);
|
||||
|
||||
const clearSourceUrl = () => setSourceUrl(null);
|
||||
|
||||
const updateUserPreferences = async (values: Partial<UserPreferences>) => {
|
||||
await window.electron.updateUserPreferences(values);
|
||||
window.electron.getUserPreferences().then((userPreferences) => {
|
||||
dispatch(setUserPreferences(userPreferences));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
updateUserPreferences,
|
||||
setCurrentCategoryIndex,
|
||||
clearSourceUrl,
|
||||
currentCategoryIndex,
|
||||
sourceUrl,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
}
|
27
src/renderer/src/declaration.d.ts
vendored
27
src/renderer/src/declaration.d.ts
vendored
|
@ -13,6 +13,7 @@ import type {
|
|||
StartGameDownloadPayload,
|
||||
RealDebridUser,
|
||||
DownloadSource,
|
||||
UserProfile,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
|
@ -70,8 +71,12 @@ declare global {
|
|||
removeGame: (gameId: number) => Promise<void>;
|
||||
deleteGameFolder: (gameId: number) => Promise<unknown>;
|
||||
getGameByObjectID: (objectID: string) => Promise<Game | null>;
|
||||
onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer;
|
||||
onGameClose: (cb: (gameId: number) => void) => () => Electron.IpcRenderer;
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
|
||||
/* User preferences */
|
||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||
|
@ -95,6 +100,7 @@ declare global {
|
|||
|
||||
/* Misc */
|
||||
openExternal: (src: string) => Promise<void>;
|
||||
isUserLoggedIn: () => Promise<boolean>;
|
||||
getVersion: () => Promise<string>;
|
||||
ping: () => string;
|
||||
getDefaultDownloadsPath: () => Promise<string>;
|
||||
|
@ -109,6 +115,23 @@ declare global {
|
|||
) => () => Electron.IpcRenderer;
|
||||
checkForUpdates: () => Promise<boolean>;
|
||||
restartAndInstallUpdate: () => Promise<void>;
|
||||
|
||||
/* Auth */
|
||||
signOut: () => Promise<void>;
|
||||
openAuthWindow: () => Promise<void>;
|
||||
getSessionHash: () => Promise<string | null>;
|
||||
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
|
||||
/* User */
|
||||
getUser: (userId: string) => Promise<UserProfile | null>;
|
||||
|
||||
/* Profile */
|
||||
getMe: () => Promise<UserProfile | null>;
|
||||
updateProfile: (
|
||||
displayName: string,
|
||||
newProfileImagePath: string | null
|
||||
) => Promise<UserProfile>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
|
|
@ -4,3 +4,5 @@ export * from "./use-preferences-slice";
|
|||
export * from "./download-slice";
|
||||
export * from "./window-slice";
|
||||
export * from "./toast-slice";
|
||||
export * from "./user-details-slice";
|
||||
export * from "./running-game-slice";
|
||||
|
|
22
src/renderer/src/features/running-game-slice.ts
Normal file
22
src/renderer/src/features/running-game-slice.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { GameRunning } from "@types";
|
||||
|
||||
export interface GameRunningState {
|
||||
gameRunning: GameRunning | null;
|
||||
}
|
||||
|
||||
const initialState: GameRunningState = {
|
||||
gameRunning: null,
|
||||
};
|
||||
|
||||
export const gameRunningSlice = createSlice({
|
||||
name: "running-game",
|
||||
initialState,
|
||||
reducers: {
|
||||
setGameRunning: (state, action: PayloadAction<GameRunning | null>) => {
|
||||
state.gameRunning = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setGameRunning } = gameRunningSlice.actions;
|
28
src/renderer/src/features/user-details-slice.ts
Normal file
28
src/renderer/src/features/user-details-slice.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import type { UserDetails } from "@types";
|
||||
|
||||
export interface UserDetailsState {
|
||||
userDetails: UserDetails | null;
|
||||
profileBackground: null | string;
|
||||
}
|
||||
|
||||
const initialState: UserDetailsState = {
|
||||
userDetails: null,
|
||||
profileBackground: null,
|
||||
};
|
||||
|
||||
export const userDetailsSlice = createSlice({
|
||||
name: "user-details",
|
||||
initialState,
|
||||
reducers: {
|
||||
setUserDetails: (state, action: PayloadAction<UserDetails | null>) => {
|
||||
state.userDetails = action.payload;
|
||||
},
|
||||
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
||||
state.profileBackground = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setUserDetails, setProfileBackground } =
|
||||
userDetailsSlice.actions;
|
|
@ -1,5 +1,7 @@
|
|||
import type { GameShop } from "@types";
|
||||
|
||||
import Color from "color";
|
||||
|
||||
export const steamUrlBuilder = {
|
||||
library: (objectID: string) =>
|
||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
|
||||
|
@ -40,3 +42,6 @@ export const buildGameDetailsPath = (
|
|||
const searchParams = new URLSearchParams({ title: game.title, ...params });
|
||||
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||
new Color(color).darken(amount).alpha(alpha).toString();
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from "./use-library";
|
|||
export * from "./use-date";
|
||||
export * from "./use-toast";
|
||||
export * from "./redux";
|
||||
export * from "./use-user-details";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { formatDistance } from "date-fns";
|
||||
import { formatDistance, subMilliseconds } from "date-fns";
|
||||
import type { FormatDistanceOptions } from "date-fns";
|
||||
import {
|
||||
ptBR,
|
||||
|
@ -52,5 +52,20 @@ export function useDate() {
|
|||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
formatDiffInMillis: (
|
||||
millis: number,
|
||||
baseDate: string | number | Date,
|
||||
options?: FormatDistanceOptions
|
||||
) => {
|
||||
try {
|
||||
return formatDistance(subMilliseconds(new Date(), millis), baseDate, {
|
||||
...options,
|
||||
locale: getDateLocale(),
|
||||
});
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
84
src/renderer/src/hooks/use-user-details.ts
Normal file
84
src/renderer/src/hooks/use-user-details.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { useCallback } from "react";
|
||||
import { average } from "color.js";
|
||||
|
||||
import { useAppDispatch, useAppSelector } from "./redux";
|
||||
import { setProfileBackground, setUserDetails } from "@renderer/features";
|
||||
import { darkenColor } from "@renderer/helpers";
|
||||
import { UserDetails } from "@types";
|
||||
|
||||
export function useUserDetails() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { userDetails, profileBackground } = useAppSelector(
|
||||
(state) => state.userDetails
|
||||
);
|
||||
|
||||
const clearUserDetails = useCallback(async () => {
|
||||
dispatch(setUserDetails(null));
|
||||
dispatch(setProfileBackground(null));
|
||||
|
||||
window.localStorage.removeItem("userDetails");
|
||||
}, [dispatch]);
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
clearUserDetails();
|
||||
|
||||
return window.electron.signOut();
|
||||
}, [clearUserDetails]);
|
||||
|
||||
const updateUserDetails = useCallback(
|
||||
async (userDetails: UserDetails) => {
|
||||
dispatch(setUserDetails(userDetails));
|
||||
|
||||
if (userDetails.profileImageUrl) {
|
||||
const output = await average(userDetails.profileImageUrl, {
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
});
|
||||
|
||||
const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
|
||||
window.localStorage.setItem(
|
||||
"userDetails",
|
||||
JSON.stringify({ ...userDetails, profileBackground })
|
||||
);
|
||||
} else {
|
||||
const profileBackground = `#151515B3`;
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
|
||||
window.localStorage.setItem(
|
||||
"userDetails",
|
||||
JSON.stringify({ ...userDetails, profileBackground })
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const fetchUserDetails = useCallback(async () => {
|
||||
return window.electron.getMe();
|
||||
}, []);
|
||||
|
||||
const patchUser = useCallback(
|
||||
async (displayName: string, imageProfileUrl: string | null) => {
|
||||
const response = await window.electron.updateProfile(
|
||||
displayName,
|
||||
imageProfileUrl
|
||||
);
|
||||
|
||||
return updateUserDetails(response);
|
||||
},
|
||||
[updateUserDetails]
|
||||
);
|
||||
|
||||
return {
|
||||
userDetails,
|
||||
fetchUserDetails,
|
||||
signOut,
|
||||
clearUserDetails,
|
||||
updateUserDetails,
|
||||
patchUser,
|
||||
profileBackground,
|
||||
};
|
||||
}
|
|
@ -27,6 +27,7 @@ import {
|
|||
import { store } from "./store";
|
||||
|
||||
import * as resources from "@locales";
|
||||
import { User } from "./pages/user/user";
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
|
@ -54,6 +55,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/user/:userId" Component={User} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
|
|
@ -138,8 +138,9 @@ export const randomizerButton = style({
|
|||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
||||
/* Scroll bar + spacing */
|
||||
right: `${9 + SPACING_UNIT * 2}px`,
|
||||
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px",
|
||||
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
|
||||
border: `solid 2px ${vars.color.border}`,
|
||||
zIndex: "1",
|
||||
backgroundColor: vars.color.background,
|
||||
":hover": {
|
||||
backgroundColor: vars.color.background,
|
||||
|
@ -149,6 +150,12 @@ export const randomizerButton = style({
|
|||
":active": {
|
||||
transform: "scale(0.98)",
|
||||
},
|
||||
":disabled": {
|
||||
boxShadow: "none",
|
||||
transform: "none",
|
||||
opacity: "0.8",
|
||||
backgroundColor: vars.color.background,
|
||||
},
|
||||
});
|
||||
|
||||
export const heroPanelSkeleton = style({
|
||||
|
|
|
@ -27,6 +27,7 @@ import { Downloader } from "@shared";
|
|||
|
||||
export function GameDetails() {
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
const [randomizerLocked, setRandomizerLocked] = useState(false);
|
||||
|
||||
const { objectID } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
@ -54,6 +55,18 @@ export function GameDetails() {
|
|||
{ fromRandomizer: "1" }
|
||||
)
|
||||
);
|
||||
|
||||
setRandomizerLocked(true);
|
||||
|
||||
const zero = performance.now();
|
||||
|
||||
requestAnimationFrame(function animateLock(time) {
|
||||
if (time - zero <= 1000) {
|
||||
requestAnimationFrame(animateLock);
|
||||
} else {
|
||||
setRandomizerLocked(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -118,7 +131,7 @@ export function GameDetails() {
|
|||
className={styles.randomizerButton}
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame}
|
||||
disabled={!randomGame || randomizerLocked}
|
||||
>
|
||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||
<Lottie
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { Game } from "@types";
|
|||
import * as styles from "./game-options-modal.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import { useDownload, useToast } from "@renderer/hooks";
|
||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||
|
||||
export interface GameOptionsModalProps {
|
||||
|
@ -21,6 +21,8 @@ export function GameOptionsModal({
|
|||
}: GameOptionsModalProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const { updateGame, setShowRepacksModal, selectGameExecutable } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
|
@ -61,7 +63,13 @@ export function GameOptionsModal({
|
|||
};
|
||||
|
||||
const handleCreateShortcut = async () => {
|
||||
await window.electron.createGameShortcut(game.id);
|
||||
window.electron.createGameShortcut(game.id).then((success) => {
|
||||
if (success) {
|
||||
showSuccessToast(t("create_shortcut_success"));
|
||||
} else {
|
||||
showErrorToast(t("create_shortcut_error"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenDownloadFolder = async () => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
|
@ -17,11 +16,9 @@ export const content = style({
|
|||
flex: "1",
|
||||
});
|
||||
|
||||
export const cards = recipe({
|
||||
base: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
transition: "all ease 0.2s",
|
||||
},
|
||||
export const cards = style({
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
transition: "all ease 0.2s",
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
|
||||
interface AddDownloadSourceModalProps {
|
||||
visible: boolean;
|
||||
|
@ -23,24 +24,31 @@ export function AddDownloadSourceModal({
|
|||
downloadCount: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setValue("");
|
||||
setIsLoading(false);
|
||||
setValidationResult(null);
|
||||
}, [visible]);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const handleValidateDownloadSource = async () => {
|
||||
const { sourceUrl } = useContext(settingsContext);
|
||||
|
||||
const handleValidateDownloadSource = useCallback(async (url: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await window.electron.validateDownloadSource(value);
|
||||
const result = await window.electron.validateDownloadSource(url);
|
||||
setValidationResult(result);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setValue("");
|
||||
setIsLoading(false);
|
||||
setValidationResult(null);
|
||||
|
||||
if (sourceUrl) {
|
||||
setValue(sourceUrl);
|
||||
handleValidateDownloadSource(sourceUrl);
|
||||
}
|
||||
}, [visible, handleValidateDownloadSource, sourceUrl]);
|
||||
|
||||
const handleAddDownloadSource = async () => {
|
||||
await window.electron.addDownloadSource(value);
|
||||
|
@ -65,7 +73,7 @@ export function AddDownloadSourceModal({
|
|||
>
|
||||
<TextField
|
||||
label={t("download_source_url")}
|
||||
placeholder="Insert a valid JSON url"
|
||||
placeholder={t("insert_valid_json_url")}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
rightContent={
|
||||
|
@ -73,7 +81,7 @@ export function AddDownloadSourceModal({
|
|||
type="button"
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={handleValidateDownloadSource}
|
||||
onClick={() => handleValidateDownloadSource(value)}
|
||||
disabled={isLoading || !value}
|
||||
>
|
||||
{t("validate_download_source")}
|
||||
|
@ -99,14 +107,16 @@ export function AddDownloadSourceModal({
|
|||
>
|
||||
<h4>{validationResult?.name}</h4>
|
||||
<small>
|
||||
Found{" "}
|
||||
{validationResult?.downloadCount.toLocaleString(undefined)}{" "}
|
||||
download options
|
||||
{t("found_download_option", {
|
||||
count: validationResult?.downloadCount,
|
||||
countFormatted:
|
||||
validationResult?.downloadCount.toLocaleString(),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<Button type="button" onClick={handleAddDownloadSource}>
|
||||
Import
|
||||
{t("import")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
import { CheckboxField } from "@renderer/components";
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
|
||||
export interface SettingsBehaviorProps {
|
||||
updateUserPreferences: (values: Partial<UserPreferences>) => void;
|
||||
}
|
||||
|
||||
export function SettingsBehavior({
|
||||
updateUserPreferences,
|
||||
}: SettingsBehaviorProps) {
|
||||
export function SettingsBehavior() {
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const { updateUserPreferences } = useContext(settingsContext);
|
||||
|
||||
const [form, setForm] = useState({
|
||||
preferQuitInsteadOfHiding: false,
|
||||
runAtStartup: false,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
import { TextField, Button, Badge } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
@ -10,6 +10,7 @@ import { AddDownloadSourceModal } from "./add-download-source-modal";
|
|||
import { useToast } from "@renderer/hooks";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
|
||||
export function SettingsDownloadSources() {
|
||||
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
||||
|
@ -18,8 +19,9 @@ export function SettingsDownloadSources() {
|
|||
const [isSyncingDownloadSources, setIsSyncingDownloadSources] =
|
||||
useState(false);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const getDownloadSources = async () => {
|
||||
|
@ -32,6 +34,10 @@ export function SettingsDownloadSources() {
|
|||
getDownloadSources();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceUrl) setShowAddDownloadSourceModal(true);
|
||||
}, [sourceUrl]);
|
||||
|
||||
const handleRemoveSource = async (id: number) => {
|
||||
await window.electron.removeDownloadSource(id);
|
||||
showSuccessToast(t("removed_download_source"));
|
||||
|
@ -63,11 +69,16 @@ export function SettingsDownloadSources() {
|
|||
[DownloadSourceStatus.Errored]: t("download_source_errored"),
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
clearSourceUrl();
|
||||
setShowAddDownloadSourceModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddDownloadSourceModal
|
||||
visible={showAddDownloadSourceModal}
|
||||
onClose={() => setShowAddDownloadSourceModal(false)}
|
||||
onClose={handleModalClose}
|
||||
onAddDownloadSource={handleAddDownloadSource}
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import ISO6391 from "iso-639-1";
|
||||
|
||||
import {
|
||||
|
@ -8,27 +8,24 @@ import {
|
|||
SelectField,
|
||||
} from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
|
||||
import { changeLanguage } from "i18next";
|
||||
import * as languageResources from "@locales";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
|
||||
interface LanguageOption {
|
||||
option: string;
|
||||
nativeName: string;
|
||||
}
|
||||
|
||||
export interface SettingsGeneralProps {
|
||||
updateUserPreferences: (values: Partial<UserPreferences>) => void;
|
||||
}
|
||||
|
||||
export function SettingsGeneral({
|
||||
updateUserPreferences,
|
||||
}: SettingsGeneralProps) {
|
||||
export function SettingsGeneral() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const { updateUserPreferences } = useContext(settingsContext);
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, CheckboxField, Link, TextField } from "@renderer/components";
|
||||
import * as styles from "./settings-real-debrid.css";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
|
||||
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
|
||||
|
||||
export interface SettingsRealDebridProps {
|
||||
updateUserPreferences: (values: Partial<UserPreferences>) => void;
|
||||
}
|
||||
|
||||
export function SettingsRealDebrid({
|
||||
updateUserPreferences,
|
||||
}: SettingsRealDebridProps) {
|
||||
export function SettingsRealDebrid() {
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const { updateUserPreferences } = useContext(settingsContext);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
useRealDebrid: false,
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "@renderer/components";
|
||||
|
||||
import * as styles from "./settings.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserPreferences } from "@types";
|
||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||
import { SettingsGeneral } from "./settings-general";
|
||||
import { SettingsBehavior } from "./settings-behavior";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { setUserPreferences } from "@renderer/features";
|
||||
|
||||
import { SettingsDownloadSources } from "./settings-download-sources";
|
||||
import {
|
||||
SettingsContextConsumer,
|
||||
SettingsContextProvider,
|
||||
} from "@renderer/context";
|
||||
|
||||
export function Settings() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const categories = [
|
||||
t("general"),
|
||||
t("behavior"),
|
||||
|
@ -23,57 +22,50 @@ export function Settings() {
|
|||
"Real-Debrid",
|
||||
];
|
||||
|
||||
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
||||
|
||||
const handleUpdateUserPreferences = async (
|
||||
values: Partial<UserPreferences>
|
||||
) => {
|
||||
await window.electron.updateUserPreferences(values);
|
||||
window.electron.getUserPreferences().then((userPreferences) => {
|
||||
dispatch(setUserPreferences(userPreferences));
|
||||
});
|
||||
};
|
||||
|
||||
const renderCategory = () => {
|
||||
if (currentCategoryIndex === 0) {
|
||||
return (
|
||||
<SettingsGeneral updateUserPreferences={handleUpdateUserPreferences} />
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 1) {
|
||||
return (
|
||||
<SettingsBehavior updateUserPreferences={handleUpdateUserPreferences} />
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 2) {
|
||||
return <SettingsDownloadSources />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsRealDebrid updateUserPreferences={handleUpdateUserPreferences} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<section className={styles.settingsCategories}>
|
||||
{categories.map((category, index) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={currentCategoryIndex === index ? "primary" : "outline"}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
<SettingsContextProvider>
|
||||
<SettingsContextConsumer>
|
||||
{({ currentCategoryIndex, setCurrentCategoryIndex }) => {
|
||||
const renderCategory = () => {
|
||||
if (currentCategoryIndex === 0) {
|
||||
return <SettingsGeneral />;
|
||||
}
|
||||
|
||||
<h2>{categories[currentCategoryIndex]}</h2>
|
||||
{renderCategory()}
|
||||
</div>
|
||||
</section>
|
||||
if (currentCategoryIndex === 1) {
|
||||
return <SettingsBehavior />;
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 2) {
|
||||
return <SettingsDownloadSources />;
|
||||
}
|
||||
|
||||
return <SettingsRealDebrid />;
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<section className={styles.settingsCategories}>
|
||||
{categories.map((category, index) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={
|
||||
currentCategoryIndex === index ? "primary" : "outline"
|
||||
}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<h2>{categories[currentCategoryIndex]}</h2>
|
||||
{renderCategory()}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}}
|
||||
</SettingsContextConsumer>
|
||||
</SettingsContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
315
src/renderer/src/pages/user/user-content.tsx
Normal file
315
src/renderer/src/pages/user/user-content.tsx
Normal file
|
@ -0,0 +1,315 @@
|
|||
import { UserGame, UserProfile } from "@types";
|
||||
import cn from "classnames";
|
||||
|
||||
import * as styles from "./user.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import {
|
||||
useAppSelector,
|
||||
useDate,
|
||||
useToast,
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
||||
import { PersonIcon, TelescopeIcon } from "@primer/octicons-react";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
import { UserEditProfileModal } from "./user-edit-modal";
|
||||
import { UserSignOutModal } from "./user-signout-modal";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
export interface ProfileContentProps {
|
||||
userProfile: UserProfile;
|
||||
updateUserProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function UserContent({
|
||||
userProfile,
|
||||
updateUserProfile,
|
||||
}: ProfileContentProps) {
|
||||
const { t, i18n } = useTranslation("user_profile");
|
||||
|
||||
const { userDetails, profileBackground, signOut } = useUserDetails();
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const numberFormatter = useMemo(() => {
|
||||
return new Intl.NumberFormat(i18n.language, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}, [i18n.language]);
|
||||
|
||||
const { formatDistance, formatDiffInMillis } = useDate();
|
||||
|
||||
const formatPlayTime = () => {
|
||||
const seconds = userProfile.totalPlayTimeInSeconds;
|
||||
const minutes = seconds / 60;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes", {
|
||||
amount: minutes.toFixed(0),
|
||||
});
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
};
|
||||
|
||||
const handleGameClick = (game: UserGame) => {
|
||||
navigate(buildGameDetailsPath(game));
|
||||
};
|
||||
|
||||
const handleEditProfile = () => {
|
||||
setShowEditProfileModal(true);
|
||||
};
|
||||
|
||||
const handleConfirmSignout = async () => {
|
||||
await signOut();
|
||||
|
||||
showSuccessToast(t("successfully_signed_out"));
|
||||
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const isMe = userDetails?.id == userProfile.id;
|
||||
|
||||
const profileContentBoxBackground = useMemo(() => {
|
||||
if (profileBackground) return profileBackground;
|
||||
/* TODO: Render background colors for other users */
|
||||
return undefined;
|
||||
}, [profileBackground]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserEditProfileModal
|
||||
visible={showEditProfileModal}
|
||||
onClose={() => setShowEditProfileModal(false)}
|
||||
updateUserProfile={updateUserProfile}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
|
||||
<UserSignOutModal
|
||||
visible={showSignOutModal}
|
||||
onClose={() => setShowSignOutModal(false)}
|
||||
onConfirm={handleConfirmSignout}
|
||||
/>
|
||||
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{gameRunning && isMe && (
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(gameRunning.objectID)}
|
||||
alt={gameRunning.title}
|
||||
className={styles.profileBackground}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: profileContentBoxBackground,
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className={styles.profileAvatarContainer}>
|
||||
{userProfile.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile.displayName}
|
||||
src={userProfile.profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
|
||||
{isMe && gameRunning && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Link to={buildGameDetailsPath(gameRunning)}>
|
||||
{gameRunning.title}
|
||||
</Link>
|
||||
</div>
|
||||
<small>
|
||||
{t("playing_for", {
|
||||
amount: formatDiffInMillis(
|
||||
gameRunning.sessionDurationInMillis,
|
||||
new Date()
|
||||
),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isMe && (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
justifyContent: "end",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Button theme="outline" onClick={handleEditProfile}>
|
||||
{t("edit_profile")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
theme="danger"
|
||||
onClick={() => setShowSignOutModal(true)}
|
||||
>
|
||||
{t("sign_out")}
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className={styles.profileContent}>
|
||||
<div className={styles.profileGameSection}>
|
||||
<h2>{t("activity")}</h2>
|
||||
|
||||
{!userProfile.recentGames.length ? (
|
||||
<div className={styles.noDownloads}>
|
||||
<div className={styles.telescopeIcon}>
|
||||
<TelescopeIcon size={24} />
|
||||
</div>
|
||||
<h2>{t("no_recent_activity_title")}</h2>
|
||||
<p style={{ fontFamily: "Fira Sans" }}>
|
||||
{t("no_recent_activity_description")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
{userProfile.recentGames.map((game) => (
|
||||
<button
|
||||
key={game.objectID}
|
||||
className={cn(styles.feedItem, styles.profileContentBox)}
|
||||
onClick={() => handleGameClick(game)}
|
||||
>
|
||||
<img
|
||||
className={styles.feedGameIcon}
|
||||
src={game.cover}
|
||||
alt={game.title}
|
||||
/>
|
||||
<div className={styles.gameInformation}>
|
||||
<h4>{game.title}</h4>
|
||||
<small>
|
||||
{t("last_time_played", {
|
||||
period: formatDistance(
|
||||
game.lastTimePlayed!,
|
||||
new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
}
|
||||
),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn(styles.contentSidebar, styles.profileGameSection)}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<h2>{t("library")}</h2>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: vars.color.border,
|
||||
height: "1px",
|
||||
}}
|
||||
/>
|
||||
<h3 style={{ fontWeight: "400" }}>
|
||||
{userProfile.libraryGames.length}
|
||||
</h3>
|
||||
</div>
|
||||
<small>{t("total_play_time", { amount: formatPlayTime() })}</small>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{userProfile.libraryGames.map((game) => (
|
||||
<button
|
||||
key={game.objectID}
|
||||
className={cn(styles.gameListItem, styles.profileContentBox)}
|
||||
onClick={() => handleGameClick(game)}
|
||||
title={game.title}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className={styles.libraryGameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.libraryGameIcon} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
147
src/renderer/src/pages/user/user-edit-modal.tsx
Normal file
147
src/renderer/src/pages/user/user-edit-modal.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
import { UserProfile } from "@types";
|
||||
import * as styles from "./user.css";
|
||||
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserEditProfileModalProps {
|
||||
userProfile: UserProfile;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
updateUserProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const UserEditProfileModal = ({
|
||||
userProfile,
|
||||
visible,
|
||||
onClose,
|
||||
updateUserProfile,
|
||||
}: UserEditProfileModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [newImagePath, setNewImagePath] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const { patchUser } = useUserDetails();
|
||||
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(userProfile.displayName);
|
||||
}, [userProfile.displayName]);
|
||||
|
||||
const handleChangeProfileAvatar = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png", "webp"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
|
||||
setNewImagePath(path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveProfile: React.FormEventHandler<HTMLFormElement> = async (
|
||||
event
|
||||
) => {
|
||||
event.preventDefault();
|
||||
setIsSaving(true);
|
||||
|
||||
patchUser(displayName, newImagePath)
|
||||
.then(async () => {
|
||||
await updateUserProfile();
|
||||
showSuccessToast(t("saved_successfully"));
|
||||
cleanFormAndClose();
|
||||
})
|
||||
.catch(() => {
|
||||
showErrorToast(t("try_again"));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSaving(false);
|
||||
});
|
||||
};
|
||||
|
||||
const resetModal = () => {
|
||||
setDisplayName(userProfile.displayName);
|
||||
setNewImagePath(null);
|
||||
};
|
||||
|
||||
const cleanFormAndClose = () => {
|
||||
resetModal();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const avatarUrl = useMemo(() => {
|
||||
if (newImagePath) return `local:${newImagePath}`;
|
||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||
return null;
|
||||
}, [newImagePath, userProfile.profileImageUrl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("edit_profile")}
|
||||
onClose={cleanFormAndClose}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSaveProfile}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
width: "350px",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileAvatarEditContainer}
|
||||
onClick={handleChangeProfileAvatar}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile.displayName}
|
||||
src={avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={96} />
|
||||
)}
|
||||
<div className={styles.editProfileImageBadge}>
|
||||
<DeviceCameraIcon size={16} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<TextField
|
||||
label={t("display_name")}
|
||||
value={displayName}
|
||||
required
|
||||
minLength={3}
|
||||
containerProps={{ style: { width: "100%" } }}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={isSaving}
|
||||
style={{ alignSelf: "end" }}
|
||||
type="submit"
|
||||
>
|
||||
{isSaving ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
40
src/renderer/src/pages/user/user-signout-modal.tsx
Normal file
40
src/renderer/src/pages/user/user-signout-modal.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./user.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserEditProfileModalProps {
|
||||
visible: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const UserSignOutModal = ({
|
||||
visible,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: UserEditProfileModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("sign_out_modal_title")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.signOutModalContent}>
|
||||
<p style={{ fontFamily: "Fira Sans" }}>{t("sign_out_modal_text")}</p>
|
||||
<div className={styles.signOutModalButtonsContainer}>
|
||||
<Button onClick={onConfirm} theme="danger">
|
||||
{t("sign_out")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} theme="primary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
41
src/renderer/src/pages/user/user-skeleton.tsx
Normal file
41
src/renderer/src/pages/user/user-skeleton.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import Skeleton from "react-loading-skeleton";
|
||||
import cn from "classnames";
|
||||
import * as styles from "./user.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const UserSkeleton = () => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
return (
|
||||
<>
|
||||
<Skeleton className={styles.profileHeaderSkeleton} />
|
||||
<div className={styles.profileContent}>
|
||||
<div className={styles.profileGameSection}>
|
||||
<h2>{t("activity")}</h2>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
height={72}
|
||||
style={{ flex: "1", width: "100%" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={cn(styles.contentSidebar, styles.profileGameSection)}>
|
||||
<h2>{t("library")}</h2>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<Skeleton key={index} style={{ aspectRatio: "1" }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
217
src/renderer/src/pages/user/user.css.ts
Normal file
217
src/renderer/src/pages/user/user.css.ts
Normal file
|
@ -0,0 +1,217 @@
|
|||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const wrapper = style({
|
||||
padding: "24px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
});
|
||||
|
||||
export const profileContentBox = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
alignItems: "center",
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
width: "100%",
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
||||
transition: "all ease 0.3s",
|
||||
});
|
||||
|
||||
export const profileAvatarContainer = style({
|
||||
width: "96px",
|
||||
height: "96px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const profileAvatarEditContainer = style({
|
||||
width: "128px",
|
||||
height: "128px",
|
||||
display: "flex",
|
||||
borderRadius: "50%",
|
||||
color: vars.color.body,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const profileAvatarEditOverlay = style({
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "#00000055",
|
||||
color: vars.color.muted,
|
||||
zIndex: 1,
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const profileInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "flex-start",
|
||||
color: "#c0c1c7",
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const profileContent = style({
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT * 4}px`,
|
||||
});
|
||||
|
||||
export const profileGameSection = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const contentSidebar = style({
|
||||
width: "100%",
|
||||
"@media": {
|
||||
"(min-width: 768px)": {
|
||||
width: "100%",
|
||||
maxWidth: "150px",
|
||||
},
|
||||
"(min-width: 1024px)": {
|
||||
maxWidth: "250px",
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const feedGameIcon = style({
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
export const libraryGameIcon = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const feedItem = style({
|
||||
color: vars.color.body,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
height: "72px",
|
||||
transition: "all ease 0.2s",
|
||||
cursor: "pointer",
|
||||
zIndex: "1",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const gameListItem = style({
|
||||
color: vars.color.body,
|
||||
transition: "all ease 0.2s",
|
||||
cursor: "pointer",
|
||||
zIndex: "1",
|
||||
overflow: "hidden",
|
||||
padding: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const gameInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
});
|
||||
|
||||
export const profileHeaderSkeleton = style({
|
||||
height: "144px",
|
||||
});
|
||||
|
||||
export const editProfileImageBadge = style({
|
||||
width: "28px",
|
||||
height: "28px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: vars.color.background,
|
||||
backgroundColor: vars.color.muted,
|
||||
position: "absolute",
|
||||
bottom: "0px",
|
||||
right: "0px",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const telescopeIcon = style({
|
||||
width: "60px",
|
||||
height: "60px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const noDownloads = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const signOutModalContent = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const signOutModalButtonsContainer = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "end",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
paddingTop: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const profileBackground = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
objectFit: "cover",
|
||||
left: "0",
|
||||
top: "0",
|
||||
borderRadius: "4px",
|
||||
});
|
45
src/renderer/src/pages/user/user.tsx
Normal file
45
src/renderer/src/pages/user/user.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { UserProfile } from "@types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { UserSkeleton } from "./user-skeleton";
|
||||
import { UserContent } from "./user-content";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import * as styles from "./user.css";
|
||||
|
||||
export const User = () => {
|
||||
const { userId } = useParams();
|
||||
const [userProfile, setUserProfile] = useState<UserProfile>();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getUserProfile = useCallback(() => {
|
||||
return window.electron.getUser(userId!).then((userProfile) => {
|
||||
if (userProfile) {
|
||||
dispatch(setHeaderTitle(userProfile.displayName));
|
||||
setUserProfile(userProfile);
|
||||
}
|
||||
});
|
||||
}, [dispatch, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
getUserProfile();
|
||||
}, [getUserProfile]);
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<div className={styles.wrapper}>
|
||||
{userProfile ? (
|
||||
<UserContent
|
||||
userProfile={userProfile}
|
||||
updateUserProfile={getUserProfile}
|
||||
/>
|
||||
) : (
|
||||
<UserSkeleton />
|
||||
)}
|
||||
</div>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
|
@ -6,6 +6,8 @@ import {
|
|||
searchSlice,
|
||||
userPreferencesSlice,
|
||||
toastSlice,
|
||||
userDetailsSlice,
|
||||
gameRunningSlice,
|
||||
} from "@renderer/features";
|
||||
|
||||
export const store = configureStore({
|
||||
|
@ -16,6 +18,8 @@ export const store = configureStore({
|
|||
userPreferences: userPreferencesSlice.reducer,
|
||||
download: downloadSlice.reducer,
|
||||
toast: toastSlice.reducer,
|
||||
userDetails: userDetailsSlice.reducer,
|
||||
gameRunning: gameRunningSlice.reducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -21,4 +21,10 @@ export const vars = createGlobalTheme(":root", {
|
|||
body: "14px",
|
||||
small: "12px",
|
||||
},
|
||||
zIndex: {
|
||||
toast: "5",
|
||||
bottomPanel: "3",
|
||||
titleBar: "4",
|
||||
backdrop: "4",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -85,6 +85,16 @@ export interface CatalogueEntry {
|
|||
repacks: GameRepack[];
|
||||
}
|
||||
|
||||
export interface UserGame {
|
||||
objectID: string;
|
||||
shop: GameShop;
|
||||
title: string;
|
||||
iconUrl: string | null;
|
||||
cover: string;
|
||||
playTimeInSeconds: number;
|
||||
lastTimePlayed: Date | null;
|
||||
}
|
||||
|
||||
export interface DownloadQueue {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
|
@ -117,6 +127,15 @@ export interface Game {
|
|||
|
||||
export type LibraryGame = Omit<Game, "repacks">;
|
||||
|
||||
export interface GameRunning {
|
||||
id: number;
|
||||
title: string;
|
||||
iconUrl: string;
|
||||
objectID: string;
|
||||
shop: GameShop;
|
||||
sessionDurationInMillis: number;
|
||||
}
|
||||
|
||||
export interface DownloadProgress {
|
||||
downloadSpeed: number;
|
||||
timeRemaining: number;
|
||||
|
@ -234,6 +253,21 @@ export interface RealDebridUser {
|
|||
expiration: string;
|
||||
}
|
||||
|
||||
export interface UserDetails {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
totalPlayTimeInSeconds: number;
|
||||
libraryGames: UserGame[];
|
||||
recentGames: UserGame[];
|
||||
}
|
||||
|
||||
export interface DownloadSource {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue