From 0954a5bf5d5b695a9371b57a03c7f1136beb686a Mon Sep 17 00:00:00 2001 From: Abraham Leiro Date: Wed, 13 May 2026 20:47:19 +0200 Subject: [PATCH] feat(crackwatch): add CrackWatch status section to game details sidebar --- src/locales/ar/translation.json | 8 +- src/locales/be/translation.json | 8 +- src/locales/bg/translation.json | 8 +- src/locales/ca/translation.json | 8 +- src/locales/cs/translation.json | 8 +- src/locales/da/translation.json | 8 +- src/locales/de/translation.json | 8 +- src/locales/en/translation.json | 8 +- src/locales/es/translation.json | 12 +- src/locales/et/translation.json | 8 +- src/locales/fa/translation.json | 8 +- src/locales/fi/translation.json | 8 +- src/locales/fr/translation.json | 8 +- src/locales/hu/translation.json | 8 +- src/locales/id/translation.json | 8 +- src/locales/it/translation.json | 8 +- src/locales/ja/translation.json | 8 +- src/locales/kk/translation.json | 8 +- src/locales/ko/translation.json | 8 +- src/locales/lv/translation.json | 8 +- src/locales/nb/translation.json | 8 +- src/locales/nl/translation.json | 8 +- src/locales/pl/translation.json | 8 +- src/locales/pt-BR/translation.json | 8 +- src/locales/pt-PT/translation.json | 8 +- src/locales/ro/translation.json | 8 +- src/locales/ru/translation.json | 8 +- src/locales/sl/translation.json | 8 +- src/locales/sv/translation.json | 8 +- src/locales/tr/translation.json | 8 +- src/locales/uk/translation.json | 8 +- src/locales/uz/translation.json | 8 +- src/locales/zh/translation.json | 8 +- .../events/catalogue/get-crackwatch-status.ts | 42 +++++ src/main/events/catalogue/index.ts | 1 + src/main/level/sublevels/crackwatch-cache.ts | 11 ++ src/main/level/sublevels/index.ts | 1 + src/main/level/sublevels/keys.ts | 1 + src/main/services/crackwatch.ts | 152 ++++++++++++++++++ src/main/services/index.ts | 1 + src/preload/index.ts | 2 + src/renderer/src/declaration.d.ts | 6 + .../sidebar/crackwatch-section.tsx | 80 +++++++++ .../pages/game-details/sidebar/sidebar.scss | 63 ++++++++ .../pages/game-details/sidebar/sidebar.tsx | 27 ++++ src/types/crackwatch.types.ts | 6 + src/types/index.ts | 1 + 47 files changed, 627 insertions(+), 35 deletions(-) create mode 100644 src/main/events/catalogue/get-crackwatch-status.ts create mode 100644 src/main/level/sublevels/crackwatch-cache.ts create mode 100644 src/main/services/crackwatch.ts create mode 100644 src/renderer/src/pages/game-details/sidebar/crackwatch-section.tsx create mode 100644 src/types/crackwatch.types.ts diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json index 8ef534f9f..d3927a8c4 100644 --- a/src/locales/ar/translation.json +++ b/src/locales/ar/translation.json @@ -190,7 +190,13 @@ "download_error_not_cached_on_real_debrid": "هذا التنزيل غير متوفر على Real-Debrid وجلب حالة التنزيل من Real-Debrid غير متاح حاليًا.", "download_error_not_cached_on_torbox": "هذا التنزيل غير متوفر على TorBox وجلب حالة التنزيل من TorBox غير متاح حاليًا.", "game_removed_from_favorites": "تمت إزالة اللعبة من المفضلة", - "game_added_to_favorites": "تمت إضافة اللعبة إلى المفضلة" + "game_added_to_favorites": "تمت إضافة اللعبة إلى المفضلة", + "crackwatch_status": "كراك ووتش", + "cracked": "مكسور", + "uncracked": "غير مكسور", + "crack_group": "المجموعة", + "crack_date": "تاريخ الكراك", + "protection": "الحماية" }, "activation": { "title": "تفعيل Hydra", diff --git a/src/locales/be/translation.json b/src/locales/be/translation.json index 14bbfe5d4..4cbf58dac 100644 --- a/src/locales/be/translation.json +++ b/src/locales/be/translation.json @@ -69,7 +69,13 @@ "change": "Змяніць", "repacks_modal_description": "Абярыце рэпак, які хочаце сцягнуць", "select_folder_hint": "Каб змяніць папку па змоўчанні, адкрыйце", - "download_now": "Сцягнуць зараз" + "download_now": "Сцягнуць зараз", + "crackwatch_status": "CrackWatch", + "cracked": "Узламана", + "uncracked": "Не ўзламана", + "crack_group": "Група", + "crack_date": "Дата ўзлому", + "protection": "Абарона" }, "activation": { "title": "Актываваць Hydra", diff --git a/src/locales/bg/translation.json b/src/locales/bg/translation.json index 731a6b61e..3ea662a31 100644 --- a/src/locales/bg/translation.json +++ b/src/locales/bg/translation.json @@ -203,7 +203,13 @@ "create_start_menu_shortcut": "Създай пряк път в старт менюто", "invalid_wine_prefix_path": "Невалиден път до Wine префикса", "invalid_wine_prefix_path_description": "Пътят до Wine префикса е невалиден. Моля, проверете го и опитайте отново.", - "missing_wine_prefix": "Wine префикс е необходим за създаване на архив в Linux" + "missing_wine_prefix": "Wine префикс е необходим за създаване на архив в Linux", + "crackwatch_status": "CrackWatch", + "cracked": "Кракнато", + "uncracked": "Некракнато", + "crack_group": "Група", + "crack_date": "Дата на крака", + "protection": "Защита" }, "activation": { "title": "Активирай Hydra", diff --git a/src/locales/ca/translation.json b/src/locales/ca/translation.json index 17524d82d..8f8564356 100644 --- a/src/locales/ca/translation.json +++ b/src/locales/ca/translation.json @@ -110,7 +110,13 @@ "download_paused": "Descàrrega en pausa", "last_downloaded_option": "Opció de l'última descàrrega", "create_shortcut_success": "Accés directe creat satisfactòriament", - "create_shortcut_error": "Error al crear l'accés directe" + "create_shortcut_error": "Error al crear l'accés directe", + "crackwatch_status": "CrackWatch", + "cracked": "Crackejat", + "uncracked": "No crackejat", + "crack_group": "Grup", + "crack_date": "Data del crack", + "protection": "Protecció" }, "activation": { "title": "Activa l'Hydra", diff --git a/src/locales/cs/translation.json b/src/locales/cs/translation.json index 1a1a68099..4627851ab 100644 --- a/src/locales/cs/translation.json +++ b/src/locales/cs/translation.json @@ -196,7 +196,13 @@ "download_error_not_cached_on_torbox": "Toto stahování není dostupné na TorBox a získávání informací o stahování z TorBox není zatím dostupné.", "game_removed_from_favorites": "Hra odebrána z oblíbených", "game_added_to_favorites": "Hra přidána do oblíbených", - "automatically_extract_downloaded_files": "Automaticky rozbalit stažené soubory" + "automatically_extract_downloaded_files": "Automaticky rozbalit stažené soubory", + "crackwatch_status": "CrackWatch", + "cracked": "Cracknuto", + "uncracked": "Nedostupný crack", + "crack_group": "Skupina", + "crack_date": "Datum cracku", + "protection": "Ochrana" }, "activation": { "title": "Aktivovat hydru", diff --git a/src/locales/da/translation.json b/src/locales/da/translation.json index 757729842..8f96eb132 100644 --- a/src/locales/da/translation.json +++ b/src/locales/da/translation.json @@ -126,7 +126,13 @@ "download": "Download", "executable_path_in_use": "Eksekverbar allerede i brug af \"{{game}}\"", "warning": "Advarsel:", - "hydra_needs_to_remain_open": "Hydra skal forblive åbent for at denne download kan gennemføres. I tilfælde af at Hydra lukker før downloaden er færdig, mister du dit fremskridt." + "hydra_needs_to_remain_open": "Hydra skal forblive åbent for at denne download kan gennemføres. I tilfælde af at Hydra lukker før downloaden er færdig, mister du dit fremskridt.", + "crackwatch_status": "CrackWatch", + "cracked": "Cracket", + "uncracked": "Ikke cracket", + "crack_group": "Gruppe", + "crack_date": "Crack-dato", + "protection": "Beskyttelse" }, "activation": { "title": "Aktivér Hydra", diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index ed47192f5..6feaa176a 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -204,7 +204,13 @@ "create_start_menu_shortcut": "Startmenü-Verknüpfung erstellen", "invalid_wine_prefix_path": "Ungültiger Wine-Präfix-Pfad", "invalid_wine_prefix_path_description": "Der Pfad zum Wine-Präfix ist ungültig. Bitte überprüfe den Pfad und versuche es erneut.", - "missing_wine_prefix": "Wine-Präfix ist erforderlich, um eine Sicherung unter Linux zu erstellen" + "missing_wine_prefix": "Wine-Präfix ist erforderlich, um eine Sicherung unter Linux zu erstellen", + "crackwatch_status": "CrackWatch", + "cracked": "Gecrackt", + "uncracked": "Nicht gecrackt", + "crack_group": "Gruppe", + "crack_date": "Crack-Datum", + "protection": "Schutz" }, "activation": { "title": "Hydra aktivieren", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 16f043a00..cec5ff88c 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -653,7 +653,13 @@ "hide": "Hide", "publisher_games": "More from {{publisher}}", "about_this_game": "About this game", - "media": "Media" + "media": "Media", + "crackwatch_status": "CrackWatch", + "cracked": "Cracked", + "uncracked": "Uncracked", + "crack_group": "Group", + "crack_date": "Crack date", + "protection": "Protection" }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index fa2477e41..785184b15 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -315,9 +315,9 @@ "playing_for_time": "Jugando desde hace {{hours}}h {{minutes}}m", "playing_for_minutes": "Jugando desde hace {{minutes}}m", "friends": "Amigos", - "online": "En l\u00ednea", + "online": "En línea", "offline": "Desconectados", - "no_friends_yet": "A\u00fan no tienes amigos agregados" + "no_friends_yet": "Aún no tienes amigos agregados" }, "bottom_panel": { "no_downloads_in_progress": "Sin descargas en progreso", @@ -633,7 +633,13 @@ "hide": "Ocultar", "publisher_games": "Más de {{publisher}}", "about_this_game": "Sobre este juego", - "media": "Multimedia" + "media": "Multimedia", + "crackwatch_status": "CrackWatch", + "cracked": "Crackeado", + "uncracked": "Sin crackear", + "crack_group": "Grupo", + "crack_date": "Fecha del crack", + "protection": "Protección" }, "activation": { "title": "Activar Hydra", diff --git a/src/locales/et/translation.json b/src/locales/et/translation.json index 5d89b9256..97596dc3b 100644 --- a/src/locales/et/translation.json +++ b/src/locales/et/translation.json @@ -162,7 +162,13 @@ "manage_files_description": "Hallake, millised failid varundatakse ja taastatakse", "select_folder": "Vali kaust", "backup_from": "Varundamine kuupäevast {{date}}", - "custom_backup_location_set": "Kohandatud varundamise asukoht määratud" + "custom_backup_location_set": "Kohandatud varundamise asukoht määratud", + "crackwatch_status": "CrackWatch", + "cracked": "Crack'itud", + "uncracked": "Crack'imata", + "crack_group": "Rühm", + "crack_date": "Crack'i kuupäev", + "protection": "Kaitse" }, "activation": { "title": "Aktiveeri Hydra", diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json index 5c67c3546..18a67016f 100644 --- a/src/locales/fa/translation.json +++ b/src/locales/fa/translation.json @@ -69,7 +69,13 @@ "change": "تغییر", "repacks_modal_description": "ریپک مورد نظر برای دانلود را انتخاب کنید", "select_folder_hint": "برای تغییر پوشه‌ی پیش‌فرض به <0>Settings بروید", - "download_now": "الان دانلود کن" + "download_now": "الان دانلود کن", + "crackwatch_status": "CrackWatch", + "cracked": "کرک‌شده", + "uncracked": "کرک‌نشده", + "crack_group": "گروه", + "crack_date": "تاریخ کرک", + "protection": "حفاظت" }, "activation": { "title": "فعال کردن هایدرا", diff --git a/src/locales/fi/translation.json b/src/locales/fi/translation.json index c0053e5ef..8069d12c5 100644 --- a/src/locales/fi/translation.json +++ b/src/locales/fi/translation.json @@ -360,7 +360,13 @@ "caption": "Tekstitys", "audio": "Ääni", "filter_by_source": "Suodata lähteen mukaan", - "no_repacks_found": "Tämän pelin lähteitä ei löytynyt" + "no_repacks_found": "Tämän pelin lähteitä ei löytynyt", + "crackwatch_status": "CrackWatch", + "cracked": "Crackattu", + "uncracked": "Ei crackattu", + "crack_group": "Ryhmä", + "crack_date": "Crackin päivämäärä", + "protection": "Suojaus" }, "activation": { "title": "Aktivoi Hydra", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 1e861150e..184148379 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -403,7 +403,13 @@ "searching_save_folder": "Recherche du dossier de sauvegarde...", "no_save_folder_found": "Aucun dossier de sauvegarde trouvé", "shortcuts_section_title": "Raccourcis", - "shortcuts_section_description": "Créer des raccourcis pour lancer rapidement ce jeu" + "shortcuts_section_description": "Créer des raccourcis pour lancer rapidement ce jeu", + "crackwatch_status": "CrackWatch", + "cracked": "Cracké", + "uncracked": "Non cracké", + "crack_group": "Groupe", + "crack_date": "Date du crack", + "protection": "Protection" }, "activation": { "title": "Activer Hydra", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 41c7c9eda..4e8f73eeb 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -403,7 +403,13 @@ "hide_original": "Eredeti elrejtése", "review_from_blocked_user": "Letiltott felhasználó véleménye", "show": "Megjelenítés", - "hide": "Elrejtés" + "hide": "Elrejtés", + "crackwatch_status": "CrackWatch", + "cracked": "Feltörve", + "uncracked": "Nincs feltörve", + "crack_group": "Csoport", + "crack_date": "Feltörés dátuma", + "protection": "Védelem" }, "activation": { "title": "Hydra Aktiválása", diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json index 7763d1a5c..cf8d71318 100644 --- a/src/locales/id/translation.json +++ b/src/locales/id/translation.json @@ -109,7 +109,13 @@ "download_paused": "Unduhan dijeda", "last_downloaded_option": "Opsi terakhir diunduh", "create_shortcut_success": "Pintasan berhasil dibuat", - "create_shortcut_error": "Gagal membuat pintasan" + "create_shortcut_error": "Gagal membuat pintasan", + "crackwatch_status": "CrackWatch", + "cracked": "Sudah di-crack", + "uncracked": "Belum di-crack", + "crack_group": "Grup", + "crack_date": "Tanggal crack", + "protection": "Proteksi" }, "activation": { "title": "Aktifkan Hydra", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 9b73a362a..e8a714142 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -403,7 +403,13 @@ "hide_original": "Nascondi originale", "review_from_blocked_user": "Recensione di un utente bloccato", "show": "Mostra", - "hide": "Nascondi" + "hide": "Nascondi", + "crackwatch_status": "CrackWatch", + "cracked": "Craccato", + "uncracked": "Non craccato", + "crack_group": "Gruppo", + "crack_date": "Data del crack", + "protection": "Protezione" }, "activation": { "title": "Attiva Hydra", diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index 778f97b5e..5132fc6de 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -534,7 +534,13 @@ "create_steam_shortcut_modal_cancel_button": "キャンセル", "delete_shortcut_success": "Steamショートカットを削除しました", "delete_shortcut_error": "Steamショートカットの削除に失敗しました", - "delete_steam_shortcut": "Steamショートカットを削除" + "delete_steam_shortcut": "Steamショートカットを削除", + "crackwatch_status": "CrackWatch", + "cracked": "クラック済み", + "uncracked": "未クラック", + "crack_group": "グループ", + "crack_date": "クラック日", + "protection": "保護" }, "activation": { "title": "Hydraをアクティベート", diff --git a/src/locales/kk/translation.json b/src/locales/kk/translation.json index fc4a6529e..9bea9d509 100644 --- a/src/locales/kk/translation.json +++ b/src/locales/kk/translation.json @@ -109,7 +109,13 @@ "download_paused": "Жүктеу тоқтатылды", "last_downloaded_option": "Соңғы жүктеу нұсқасы", "create_shortcut_success": "Жарлық жасалды", - "create_shortcut_error": "Жарлық жасау мүмкін болмады" + "create_shortcut_error": "Жарлық жасау мүмкін болмады", + "crackwatch_status": "CrackWatch", + "cracked": "Бұзылған", + "uncracked": "Бұзылмаған", + "crack_group": "Топ", + "crack_date": "Бұзу күні", + "protection": "Қорғаныс" }, "activation": { "title": "Hydra-ны белсендіру", diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 12a569940..a844a8024 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -69,7 +69,13 @@ "change": "바꾸기", "repacks_modal_description": "다운로드 할 리팩을 선택해 주세요", "select_folder_hint": "기본 폴더를 바꾸려면 <0>설정으로 가세요", - "download_now": "지금 다운로드" + "download_now": "지금 다운로드", + "crackwatch_status": "CrackWatch", + "cracked": "크랙됨", + "uncracked": "크랙되지 않음", + "crack_group": "그룹", + "crack_date": "크랙 날짜", + "protection": "보호" }, "activation": { "title": "Hydra 실행", diff --git a/src/locales/lv/translation.json b/src/locales/lv/translation.json index 102c08a4e..e0031417b 100644 --- a/src/locales/lv/translation.json +++ b/src/locales/lv/translation.json @@ -360,7 +360,13 @@ "caption": "Subtitri", "audio": "Audio", "filter_by_source": "Filtrēt pēc avota", - "no_repacks_found": "Avoti šai spēlei nav atrasti" + "no_repacks_found": "Avoti šai spēlei nav atrasti", + "crackwatch_status": "CrackWatch", + "cracked": "Uzlauzts", + "uncracked": "Nav uzlauzts", + "crack_group": "Grupa", + "crack_date": "Uzlaušanas datums", + "protection": "Aizsardzība" }, "activation": { "title": "Aktivizēt Hydra", diff --git a/src/locales/nb/translation.json b/src/locales/nb/translation.json index f869d6098..99fc845d2 100644 --- a/src/locales/nb/translation.json +++ b/src/locales/nb/translation.json @@ -126,7 +126,13 @@ "download": "Last ned", "executable_path_in_use": "Kjørbar fil blir allerede brukt av \"{{game}}\"", "warning": "Advarsel:", - "hydra_needs_to_remain_open": "Hydra skal forbli åpent for at denne nedlastingen kan gjennomføres. I tilfelle av at Hydra lukker før nedlastingen er ferdig, mister du fremskrittet ditt." + "hydra_needs_to_remain_open": "Hydra skal forbli åpent for at denne nedlastingen kan gjennomføres. I tilfelle av at Hydra lukker før nedlastingen er ferdig, mister du fremskrittet ditt.", + "crackwatch_status": "CrackWatch", + "cracked": "Cracket", + "uncracked": "Ikke cracket", + "crack_group": "Gruppe", + "crack_date": "Crack-dato", + "protection": "Beskyttelse" }, "activation": { "title": "Aktivér Hydra", diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json index ff2080a5c..cd605d23e 100644 --- a/src/locales/nl/translation.json +++ b/src/locales/nl/translation.json @@ -69,7 +69,13 @@ "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", - "download_now": "Download nu" + "download_now": "Download nu", + "crackwatch_status": "CrackWatch", + "cracked": "Gekraakt", + "uncracked": "Niet gekraakt", + "crack_group": "Groep", + "crack_date": "Kraakdatum", + "protection": "Beveiliging" }, "activation": { "title": "Activeer Hydra", diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json index 36b8fa26a..457d46ed9 100644 --- a/src/locales/pl/translation.json +++ b/src/locales/pl/translation.json @@ -77,7 +77,13 @@ "previous_screenshot": "Poprzedni zrzut ekranu", "next_screenshot": "Następny zrzut ekranu", "screenshot": "Zrzut ekranu {{number}}", - "open_screenshot": "Otwórz zrzut ekranu {{number}}" + "open_screenshot": "Otwórz zrzut ekranu {{number}}", + "crackwatch_status": "CrackWatch", + "cracked": "Crackowane", + "uncracked": "Niecrackowane", + "crack_group": "Grupa", + "crack_date": "Data cracka", + "protection": "Ochrona" }, "activation": { "title": "Aktywuj Hydra", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index d836fdd3a..0324fa3fc 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -392,7 +392,13 @@ "rating_count": "Avaliação", "review_from_blocked_user": "Avaliação de usuário bloqueado", "show": "Mostrar", - "hide": "Ocultar" + "hide": "Ocultar", + "crackwatch_status": "CrackWatch", + "cracked": "Crackeado", + "uncracked": "Sem crack", + "crack_group": "Grupo", + "crack_date": "Data do crack", + "protection": "Proteção" }, "activation": { "title": "Ativação", diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 8f4906d22..a6877e96f 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -392,7 +392,13 @@ "shortcuts_section_description": "Criar atalhos para iniciar este jogo rapidamente", "source_offline": "Fonte está offline", "source_online": "Fonte está online", - "source_partial": "Alguns links estão offline" + "source_partial": "Alguns links estão offline", + "crackwatch_status": "CrackWatch", + "cracked": "Crackado", + "uncracked": "Sem crack", + "crack_group": "Grupo", + "crack_date": "Data do crack", + "protection": "Proteção" }, "activation": { "title": "Ativação", diff --git a/src/locales/ro/translation.json b/src/locales/ro/translation.json index e62e7c3df..d9e0f8ea4 100644 --- a/src/locales/ro/translation.json +++ b/src/locales/ro/translation.json @@ -81,7 +81,13 @@ "screenshot": "Captură de ecran {{number}}", "open_screenshot": "Deschide captura de ecran {{number}}", "download_settings": "Setări de descărcare", - "downloader": "Program de descărcare" + "downloader": "Program de descărcare", + "crackwatch_status": "CrackWatch", + "cracked": "Spart", + "uncracked": "Nespart", + "crack_group": "Grup", + "crack_date": "Data spargerii", + "protection": "Protecție" }, "activation": { "title": "Activează Hydra", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index a7c01bc77..8427cffa8 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -395,7 +395,13 @@ "hide_original": "Скрыть оригинал", "review_from_blocked_user": "Отзыв от заблокированного пользователя", "show": "Показать", - "hide": "Скрыть" + "hide": "Скрыть", + "crackwatch_status": "CrackWatch", + "cracked": "Взломана", + "uncracked": "Не взломана", + "crack_group": "Группа", + "crack_date": "Дата взлома", + "protection": "Защита" }, "activation": { "title": "Активировать Hydra", diff --git a/src/locales/sl/translation.json b/src/locales/sl/translation.json index 904c9330c..7699657bf 100644 --- a/src/locales/sl/translation.json +++ b/src/locales/sl/translation.json @@ -384,7 +384,13 @@ "hide_original": "Skrij original", "review_from_blocked_user": "Mnenje blokiranega uporabnika", "show": "Pokaži", - "hide": "Skrij" + "hide": "Skrij", + "crackwatch_status": "CrackWatch", + "cracked": "Vlomljeno", + "uncracked": "Ni vlomljeno", + "crack_group": "Skupina", + "crack_date": "Datum vloma", + "protection": "Zaščita" }, "activation": { "title": "Aktiviraj Hydra", diff --git a/src/locales/sv/translation.json b/src/locales/sv/translation.json index 6f910c4b2..78524f25d 100644 --- a/src/locales/sv/translation.json +++ b/src/locales/sv/translation.json @@ -203,7 +203,13 @@ "create_start_menu_shortcut": "Skapa genväg i Startmenyn", "invalid_wine_prefix_path": "Ogiltig sökväg för Wine-prefix", "invalid_wine_prefix_path_description": "Sökvägen till Wine-prefixet är ogiltig. Kontrollera sökvägen och försök igen.", - "missing_wine_prefix": "Wine-prefix krävs för att skapa en säkerhetskopia på Linux" + "missing_wine_prefix": "Wine-prefix krävs för att skapa en säkerhetskopia på Linux", + "crackwatch_status": "CrackWatch", + "cracked": "Crackad", + "uncracked": "Inte crackad", + "crack_group": "Grupp", + "crack_date": "Crack-datum", + "protection": "Skydd" }, "activation": { "title": "Aktivera Hydra", diff --git a/src/locales/tr/translation.json b/src/locales/tr/translation.json index d265493e4..187e20f32 100644 --- a/src/locales/tr/translation.json +++ b/src/locales/tr/translation.json @@ -369,7 +369,13 @@ "hide_original": "Orijinali gizle", "review_from_blocked_user": "Engellenen kullanıcıdan gelen inceleme", "show": "Göster", - "hide": "Gizle" + "hide": "Gizle", + "crackwatch_status": "CrackWatch", + "cracked": "Kırıldı", + "uncracked": "Kırılmadı", + "crack_group": "Grup", + "crack_date": "Kırılma tarihi", + "protection": "Koruma" }, "activation": { "title": "Hydra'yı Etkinleştir", diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json index c811763c7..8a0c98951 100644 --- a/src/locales/uk/translation.json +++ b/src/locales/uk/translation.json @@ -356,7 +356,13 @@ "caption": "Субтитри", "audio": "Аудіо", "filter_by_source": "Фільтр за джерелом", - "no_repacks_found": "Джерела для цієї гри не знайдено" + "no_repacks_found": "Джерела для цієї гри не знайдено", + "crackwatch_status": "CrackWatch", + "cracked": "Зламано", + "uncracked": "Не зламано", + "crack_group": "Група", + "crack_date": "Дата злому", + "protection": "Захист" }, "activation": { "title": "Активувати Hydra", diff --git a/src/locales/uz/translation.json b/src/locales/uz/translation.json index 95e68e7c7..7b7f70e18 100644 --- a/src/locales/uz/translation.json +++ b/src/locales/uz/translation.json @@ -167,7 +167,13 @@ "download_error_not_cached_on_torbox": "Bu yuklab olish TorBox'da mavjud emas, va TorBox'dan yuklab olish holatini olish hozircha mumkin emas.", "game_added_to_favorites": "O'yin sevimlilarga qo'shildi", "game_removed_from_favorites": "O'yin sevimlilardan olib tashlandi", - "automatically_extract_downloaded_files": "Yuklab olingan fayllarni avtomatik chiqarish" + "automatically_extract_downloaded_files": "Yuklab olingan fayllarni avtomatik chiqarish", + "crackwatch_status": "CrackWatch", + "cracked": "Crack qilingan", + "uncracked": "Crack qilinmagan", + "crack_group": "Guruh", + "crack_date": "Crack sanasi", + "protection": "Himoya" }, "activation": { "title": "Hydra'ni faollashtirish", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 05f4399f3..dfd15f4f7 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -365,7 +365,13 @@ "would_you_recommend_this_game": "您想为此游戏留下评价吗?", "write_review_placeholder": "分享您对本游戏的看法...", "yes": "是", - "you_seemed_to_enjoy_this_game": "您似乎很喜欢这款游戏" + "you_seemed_to_enjoy_this_game": "您似乎很喜欢这款游戏", + "crackwatch_status": "CrackWatch", + "cracked": "已破解", + "uncracked": "未破解", + "crack_group": "破解组", + "crack_date": "破解日期", + "protection": "保护" }, "activation": { "title": "激活 Hydra", diff --git a/src/main/events/catalogue/get-crackwatch-status.ts b/src/main/events/catalogue/get-crackwatch-status.ts new file mode 100644 index 000000000..e4f5a2fef --- /dev/null +++ b/src/main/events/catalogue/get-crackwatch-status.ts @@ -0,0 +1,42 @@ +import type { CrackWatchStatus, GameShop } from "@types"; +import { registerEvent } from "../register-event"; +import { CrackWatchService } from "@main/services"; +import { crackwatchCacheSublevel, levelKeys } from "@main/level"; + +const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 24; // 24 hours + +const getCrackWatchStatus = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop, + title: string +): Promise => { + if (shop !== "steam" || !title) { + return null; + } + + const cached = await crackwatchCacheSublevel.get( + levelKeys.game(shop, objectId) + ); + + if (cached && cached.updatedAt + LOCAL_CACHE_EXPIRATION > Date.now()) { + const { updatedAt: _updatedAt, ...status } = cached; + return status; + } + + const status = await CrackWatchService.getStatusByTitleAndAppId( + title, + objectId + ); + + if (status) { + await crackwatchCacheSublevel.put(levelKeys.game(shop, objectId), { + ...status, + updatedAt: Date.now(), + }); + } + + return status; +}; + +registerEvent("getCrackWatchStatus", getCrackWatchStatus); diff --git a/src/main/events/catalogue/index.ts b/src/main/events/catalogue/index.ts index a6e87e28f..a5b817338 100644 --- a/src/main/events/catalogue/index.ts +++ b/src/main/events/catalogue/index.ts @@ -2,4 +2,5 @@ import "./get-game-assets"; import "./get-game-repacks"; import "./get-game-shop-details"; import "./get-game-stats"; +import "./get-crackwatch-status"; import "./get-random-game"; diff --git a/src/main/level/sublevels/crackwatch-cache.ts b/src/main/level/sublevels/crackwatch-cache.ts new file mode 100644 index 000000000..e150ed5d2 --- /dev/null +++ b/src/main/level/sublevels/crackwatch-cache.ts @@ -0,0 +1,11 @@ +import type { CrackWatchStatus } from "@types"; + +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const crackwatchCacheSublevel = db.sublevel< + string, + CrackWatchStatus & { updatedAt: number } +>(levelKeys.crackwatchCache, { + valueEncoding: "json", +}); diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts index 1ab42bf54..884f309b3 100644 --- a/src/main/level/sublevels/index.ts +++ b/src/main/level/sublevels/index.ts @@ -3,6 +3,7 @@ export * from "./games"; export * from "./game-shop-assets"; export * from "./game-shop-cache"; export * from "./game-stats-cache"; +export * from "./crackwatch-cache"; export * from "./game-achievements"; export * from "./keys"; export * from "./themes"; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts index f581cf6ef..3f4dac94c 100644 --- a/src/main/level/sublevels/keys.ts +++ b/src/main/level/sublevels/keys.ts @@ -8,6 +8,7 @@ export const levelKeys = { themes: "themes", gameShopAssets: "gameShopAssets", gameStatsCache: "gameStatsAssets", + crackwatchCache: "crackwatchCache", gameShopCache: "gameShopCache", gameShopCacheItem: (shop: GameShop, objectId: string, language: string) => `${shop}:${objectId}:${language}`, diff --git a/src/main/services/crackwatch.ts b/src/main/services/crackwatch.ts new file mode 100644 index 000000000..0724611be --- /dev/null +++ b/src/main/services/crackwatch.ts @@ -0,0 +1,152 @@ +import axios, { type AxiosInstance } from "axios"; + +import type { CrackWatchStatus } from "@types"; +import { logger } from "./logger"; + +const NUXT_DATA_REGEX = + /]*id="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/; + +const titleToSlug = (title: string): string => + title + .toLowerCase() + .replace(/['’']/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + +const derefNuxtState = (arr: unknown[]): ((value: unknown) => unknown) => { + const cache = new Map(); + const resolve = (value: unknown, depth: number): unknown => { + if (depth > 32) return null; + if (typeof value === "number") { + if (!Number.isInteger(value) || value < 0 || value >= arr.length) { + return value; + } + if (cache.has(value)) return cache.get(value); + cache.set(value, null); // break cycles + const resolved = resolve(arr[value], depth + 1); + cache.set(value, resolved); + return resolved; + } + if (value === null || typeof value !== "object") return value; + if (Array.isArray(value)) { + return value.map((v) => resolve(v, depth + 1)); + } + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + out[k] = resolve(v, depth + 1); + } + return out; + }; + return (value) => resolve(value, 0); +}; + +interface GameStatusEntry { + slug?: string; + title?: string; + protections?: string | null; + hacked_groups_en?: string | null; + crack_date?: string | null; + steam_prod_id?: number | string | null; + description_en?: string | null; + specs_info?: unknown; +} + +const isMainGameEntry = (entry: GameStatusEntry): boolean => + entry !== null && + typeof entry === "object" && + // Related-game cards lack description_en/specs_info — only the focused game has them. + (entry.description_en !== undefined || entry.specs_info !== undefined); + +class CrackWatchServiceClass { + private readonly client: AxiosInstance; + + constructor() { + this.client = axios.create({ + baseURL: "https://gamestatus.info", + timeout: 8000, + headers: { + Accept: "text/html,application/xhtml+xml", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Hydra/CrackWatch", + }, + }); + } + + async getStatusByTitleAndAppId( + title: string, + appId: string + ): Promise { + const slug = titleToSlug(title); + if (!slug) return null; + + try { + const { data: html } = await this.client.get(`/${slug}/en`, { + responseType: "text", + validateStatus: (s) => s >= 200 && s < 400, + }); + + const match = NUXT_DATA_REGEX.exec(html); + if (!match) { + logger.warn(`CrackWatch: __NUXT_DATA__ not found for slug=${slug}`); + return null; + } + + const arr = JSON.parse(match[1]) as unknown[]; + if (!Array.isArray(arr)) return null; + + const deref = derefNuxtState(arr); + + let game: GameStatusEntry | null = null; + for (const entry of arr) { + if ( + entry && + typeof entry === "object" && + !Array.isArray(entry) && + "slug" in entry && + "crack_date" in entry && + "steam_prod_id" in entry && + isMainGameEntry(entry as GameStatusEntry) + ) { + const resolved = deref(entry) as GameStatusEntry; + if (String(resolved.steam_prod_id) === String(appId)) { + game = resolved; + break; + } + } + } + + if (!game) { + logger.info( + `CrackWatch: no entry matched appId=${appId} on slug=${slug}` + ); + return null; + } + + const crackDate = + typeof game.crack_date === "string" && game.crack_date.length > 0 + ? game.crack_date + : null; + + return { + isCracked: crackDate !== null, + crackDate, + crackGroup: + typeof game.hacked_groups_en === "string" && game.hacked_groups_en + ? game.hacked_groups_en + : null, + protection: + typeof game.protections === "string" && game.protections + ? game.protections + : null, + }; + } catch (err) { + logger.warn( + `CrackWatch lookup failed for slug=${slug} appId=${appId}`, + err + ); + return null; + } + } +} + +export const CrackWatchService = new CrackWatchServiceClass(); diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 3347b9bca..2308ec79a 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -26,3 +26,4 @@ export * from "./download-sources-checker"; export * from "./notifications/local-notifications"; export * from "./rom-save-manager"; export * from "./rom-scraper"; +export * from "./crackwatch"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 0378ee3fd..6b3540624 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -83,6 +83,8 @@ contextBridge.exposeInMainWorld("electron", { getRandomGame: () => ipcRenderer.invoke("getRandomGame"), getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), + getCrackWatchStatus: (objectId: string, shop: GameShop, title: string) => + ipcRenderer.invoke("getCrackWatchStatus", objectId, shop, title), getGameAssets: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameAssets", objectId, shop), getGameRepacks: (shop: GameShop, objectId: string) => diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index ec65266a5..9446510af 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -40,6 +40,7 @@ import type { RomEntry, RssFeed, NewsArticle, + CrackWatchStatus, } from "@types"; import type { AxiosProgressEvent } from "axios"; @@ -86,6 +87,11 @@ declare global { ) => Promise; getRandomGame: () => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; + getCrackWatchStatus: ( + objectId: string, + shop: GameShop, + title: string + ) => Promise; getGameAssets: ( objectId: string, shop: GameShop diff --git a/src/renderer/src/pages/game-details/sidebar/crackwatch-section.tsx b/src/renderer/src/pages/game-details/sidebar/crackwatch-section.tsx new file mode 100644 index 000000000..e60b8ff5f --- /dev/null +++ b/src/renderer/src/pages/game-details/sidebar/crackwatch-section.tsx @@ -0,0 +1,80 @@ +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import { useTranslation } from "react-i18next"; +import type { CrackWatchStatus } from "@types"; +import { SidebarSection } from "../sidebar-section/sidebar-section"; +import { useDate } from "@renderer/hooks"; +import "./sidebar.scss"; + +export interface CrackWatchSectionProps { + data: CrackWatchStatus | null; + isLoading: boolean; +} + +export function CrackWatchSection({ data, isLoading }: CrackWatchSectionProps) { + const { t } = useTranslation("game_details"); + const { formatDate } = useDate(); + + if (!isLoading && !data) return null; + + return ( + + +
+ {data ? ( + <> + + {data.isCracked ? t("cracked") : t("uncracked")} + + +
+ {data.protection && ( +
+ {t("protection")} + {data.protection} +
+ )} + + {data.crackGroup && ( +
+ + {t("crack_group")} + + {data.crackGroup} +
+ )} + + {data.crackDate && ( +
+ {t("crack_date")} + + {formatDate(data.crackDate)} + +
+ )} +
+ + ) : ( + <> + +
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+ + )} +
+
+
+ ); +} diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.scss b/src/renderer/src/pages/game-details/sidebar/sidebar.scss index 324904d17..20bd80a68 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.scss +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.scss @@ -104,6 +104,69 @@ } } +.crackwatch { + &__container { + padding: calc(globals.$spacing-unit * 2); + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 1.5); + } + + &__badge { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: calc(globals.$spacing-unit / 2); + padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 1.5); + border-radius: 999px; + font-weight: 700; + font-size: globals.$small-font-size; + text-transform: uppercase; + letter-spacing: 0.5px; + + &--cracked { + background: rgba(46, 160, 67, 0.15); + color: #3fb950; + border: 1px solid rgba(46, 160, 67, 0.4); + } + + &--uncracked { + background: rgba(248, 81, 73, 0.15); + color: #f85149; + border: 1px solid rgba(248, 81, 73, 0.4); + } + } + + &__details { + display: flex; + flex-direction: column; + gap: globals.$spacing-unit; + } + + &__row { + display: flex; + justify-content: space-between; + align-items: center; + gap: globals.$spacing-unit; + font-size: globals.$small-font-size; + } + + &__label { + color: globals.$muted-color; + } + + &__value { + color: #fff; + font-weight: 600; + text-align: right; + } + + &__skeleton { + border-radius: 8px; + height: 24px; + } +} + .stats { &__section { display: flex; diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index a1b1282bf..4830ca1b5 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -1,5 +1,6 @@ import { useContext, useEffect, useState } from "react"; import type { + CrackWatchStatus, HowLongToBeatCategory, SteamAppDetails, UserAchievement, @@ -16,6 +17,7 @@ import { StarIcon, } from "@primer/octicons-react"; import { HowLongToBeatSection } from "./how-long-to-beat-section"; +import { CrackWatchSection } from "./crackwatch-section"; import { SidebarSection } from "../sidebar-section/sidebar-section"; import { buildGameAchievementPath } from "@renderer/helpers"; import "./sidebar.scss"; @@ -61,6 +63,11 @@ export function Sidebar() { data: HowLongToBeatCategory[] | null; }>({ isLoading: true, data: null }); + const [crackwatch, setCrackwatch] = useState<{ + isLoading: boolean; + data: CrackWatchStatus | null; + }>({ isLoading: true, data: null }); + const { userDetails } = useUserDetails(); const [activeRequirement, setActiveRequirement] = useState("minimum"); @@ -93,6 +100,21 @@ export function Sidebar() { } }, [objectId, shop]); + useEffect(() => { + if (objectId) { + setCrackwatch({ isLoading: true, data: null }); + + window.electron + .getCrackWatchStatus(objectId, shop, gameTitle) + .then((data) => { + setCrackwatch({ isLoading: false, data }); + }) + .catch(() => { + setCrackwatch({ isLoading: false, data: null }); + }); + } + }, [objectId, shop, gameTitle]); + return (