Compare commits

...

23 Commits

Author SHA1 Message Date
Chubby Granny Chaser
a439095260 fix: fixing hu translation
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-12 22:51:41 +01:00
Chubby Granny Chaser
5ff263e8c8 Merge pull request #1807 from Stormm232/main
Hungarian Translation: Aligned to latest update.
2025-10-12 22:46:06 +01:00
Kiwo.2
864c47ee46 Missing "," fix 2025-10-12 23:45:34 +02:00
Kiwo.2
523e19d97a Merge branch 'main' into main 2025-10-12 23:41:08 +02:00
Zamitto
03b2c8d07f fix: missing abortController after assets promise 2025-10-12 18:32:06 -03:00
Kiwo.2
d296830533 Matching *HU* translation to Latest 2025-10-12 23:30:52 +02:00
Kiwo.2
7fc4e17547 Merge branch 'hydralauncher:main' into main 2025-10-12 23:26:16 +02:00
Chubby Granny Chaser
636cf287c9 Merge branch 'main' of github.com:hydralauncher/hydra 2025-10-12 22:06:16 +01:00
Chubby Granny Chaser
84be238988 ci: version bump 2025-10-12 22:05:53 +01:00
Chubby Granny Chaser
494a2a0da5 Merge pull request #1806 from hydralauncher/fix/game_asset_changing_path
Fix: game asset changing path
2025-10-12 20:56:00 +01:00
Moyase
4e912b3b8d Merge branch 'main' into fix/game_asset_changing_path 2025-10-12 22:52:45 +03:00
Moyasee
e71211f1aa Fix: extracted ternary operations 2025-10-12 22:51:35 +03:00
Moyasee
a946f3bd5a Fix: extracted ternary operations 2025-10-12 22:48:33 +03:00
Chubby Granny Chaser
374b62983b feat: adding 2h constraint 2025-10-12 20:48:13 +01:00
Moyasee
0cd4c3ccf6 Fix: extracted ternary operations 2025-10-12 22:45:20 +03:00
Moyasee
7b97663b3a Merge branch 'fix/game_asset_changing_path' of https://github.com/hydralauncher/hydra into fix/game_asset_changing_path 2025-10-12 22:40:07 +03:00
Moyasee
68e2e2a772 Fix: conditional structure 2025-10-12 22:39:26 +03:00
Moyase
39979292e2 Merge branch 'main' into fix/game_asset_changing_path 2025-10-12 22:37:28 +03:00
Moyasee
60ae7d40fa Fix: Image path persists upon clearing image 2025-10-12 22:34:05 +03:00
Chubby Granny Chaser
63b6b0b44e Merge pull request #1805 from hydralauncher/feat/reviews-and-commenting
fix: fixing search on sources modal
2025-10-12 20:17:22 +01:00
Moyasee
82c0dc0d97 Merge branch 'main' of https://github.com/hydralauncher/hydra into fix/game_asset_changing_path 2025-10-12 22:13:09 +03:00
Moyasee
1cba3f350c formatting 2025-10-12 22:12:15 +03:00
Kiwo.2
aa4def327a New lines added for update 2025-10-07 19:39:25 +02:00
6 changed files with 508 additions and 292 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.6.8",
"version": "3.7.0",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",

View File

@@ -32,8 +32,8 @@
"show_playable_only_tooltip": "Csak játszható játék mutatása",
"custom_game_modal": "Saját játék hozzáadása:",
"custom_game_modal_description": "Adj meg egy futtatható fájlt",
"custom_game_modal_executable_path": "A fájl útvonala",
"custom_game_modal_select_executable": "Az útvonal",
"custom_game_modal_executable_path": "Futtatható fájl",
"custom_game_modal_select_executable": "Fájl útvonala",
"custom_game_modal_title": "Játékcím",
"custom_game_modal_enter_title": "Játék elnevezése",
"custom_game_modal_browse": "Tallózás",
@@ -43,7 +43,7 @@
"custom_game_modal_success": "Saját játék sikeresen hozzáadva",
"custom_game_modal_failed": "Saját játék hozzáadása sikertelen",
"custom_game_modal_executable": "Futtatható fájl",
"edit_game_modal": "Játékmegjelenés",
"edit_game_modal": "Játékmegjelenítése:",
"edit_game_modal_description": "Játékcím és vizuális elemek módosítása",
"edit_game_modal_title": "Játékcím",
"edit_game_modal_enter_title": "Játék elnevezése",
@@ -70,7 +70,25 @@
"edit_game_modal_icon_resolution": "Ajánlott felbontás: 256x256px",
"edit_game_modal_logo_resolution": "Ajánlott felbontás: 640x360px",
"edit_game_modal_hero_resolution": "Ajánlott felbontás: 1920x620px",
"edit_game_modal_assets": "Vizuális elemek:"
"edit_game_modal_assets": "Vizuális elemek:",
"edit_game_modal_drop_icon_image_here": "Húzd ide az ikon képét",
"edit_game_modal_drop_logo_image_here": "Húzd ide a logó képét",
"edit_game_modal_drop_hero_image_here": "Húzd ide a borítókép képét",
"edit_game_modal_drop_to_replace_icon": "Ikon kicserélése ráhúzással",
"edit_game_modal_drop_to_replace_logo": "Logó kicserélése ráhúzással",
"edit_game_modal_drop_to_replace_hero": "Borítókép kicserélése ráhúzással",
"install_decky_plugin": "Decky Plugin Telepítése",
"update_decky_plugin": "Decky Plugin Frissítése",
"decky_plugin_installed_version": "Decky Plugin (v{{version}})",
"install_decky_plugin_title": "Telepítsd a Hydra Decky Plugint",
"install_decky_plugin_message": "Ez letölti és telepíteni fogja a Hydra plugint a Decky Loaderhez. Előfordulhat, hogy rendszergazdai jogosultságra lesz szükség. Folytatod?",
"update_decky_plugin_title": "Hydra Decky Plugin Frissítése",
"update_decky_plugin_message": "Egy új verzió elérhető a Hydra Decky Pluginhoz. Szeretnéd frissíteni?",
"decky_plugin_installed": "Decky plugin v{{version}} sikeresen telepítve",
"decky_plugin_installation_failed": "Decky plugin telepítése sikertelen: {{error}}",
"decky_plugin_installation_error": "Decky plugin telepítése hibával járt el: {{error}}",
"confirm": "Megerősít",
"cancel": "Mégse"
},
"header": {
"search": "Keresés",
@@ -120,7 +138,7 @@
"downloading_metadata": "Metaadat letöltése",
"filter": "Repackek szűrése",
"requirements": "Rendszerkövetelmények",
"minimum": "Minimum",
"minimum": "Minimális",
"recommended": "Ajánlott",
"paused": "Szüneteltetve",
"release_date": "Megjelenés: {{date}}",
@@ -144,7 +162,7 @@
"playing_now": "Játékban: ",
"change": "Változtatás",
"repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni",
"select_folder_hint": "Hogy megváltoztasd a letöltési mappát, menj a <0>Beállítások</0> menüjébe",
"select_folder_hint": "A letöltési mappát a <0>Beállítások</0> menüjében változtathatod meg",
"download_now": "Letöltés",
"no_shop_details": "A bolt adatai nem érhetőek el.",
"download_options": "Letöltési opciók",
@@ -160,17 +178,19 @@
"open_folder": "Mappa megnyitása",
"open_download_location": "Letöltött fájlok megtekintése",
"create_shortcut": "Asztali parancsikon létrehozása",
"create_shortcut_simple": "Parancsikon létrehozása",
"clear": "Visszavon",
"remove_files": "Fájlok eltávolítása",
"remove_from_library_title": "Biztos vagy ebben?",
"remove_from_library_description": "Ezzel eltávolítod a játékot {{game}} a könyvtáradból",
"options": "Beállítások",
"properties": "További beállítások",
"executable_section_title": "Futtatható fájl",
"executable_section_description": "A fájl amely futtatásra fog kerülni amikor a \"Játék\" lenyomásra kerül",
"downloads_section_title": "Letöltések",
"downloads_section_description": "Csekkold le a játék frissítéseit vagy más verzióit",
"danger_zone_section_title": "Veszélyzóna",
"danger_zone_section_description": "Távolítsd el a játékot könyvtáradból, vagy a fájlokat amit a Hydra töltött le",
"danger_zone_section_description": "Itt eltávolítható a játék a könyvtáradból, vagy a fájlok amelyek a Hydra által lettek letöltve",
"download_in_progress": "Letöltés folyamatban",
"download_paused": "Letöltés szüneteltetve",
"last_downloaded_option": "Utoljára letöltött",
@@ -178,13 +198,21 @@
"create_shortcut_success": "A parancsikon létrehozása sikeres",
"you_might_need_to_restart_steam": "Lehetséges hogy újrakell indítsd a Steamet hogy lásd a változást.",
"create_shortcut_error": "Hiba lépett fel létrehozás közben",
"nsfw_content_title": "Ez a játék nem megfelelő tartalmat tartalmaz.",
"nsfw_content_description": "{{title}} tartalmaz tartalmat amely nem megfelelő minden korosztálynak. Biztosan folytatni szeretnéd?",
"add_to_favorites": "Kedvencekhez adás",
"remove_from_favorites": "Eltávolítás a kedvencek közül",
"failed_update_favorites": "Kedvencek frissítése sikertelen",
"game_removed_from_library": "Játék eltávolítva a könyvtárból",
"failed_remove_from_library": "Játék eltávolítása a könyvtárból sikertelen",
"files_removed_success": "Fájlok eltávolítása sikeres",
"failed_remove_files": "Fájlok eltávolítása sikertelen",
"nsfw_content_title": "Ez a játék tartalmaz nem megfelelő tartalmat",
"nsfw_content_description": "A(z) {{title}} tartalma lehetséges hogy nem megfelelő minden korosztály számára. Biztosan folytatni szeretnéd?",
"allow_nsfw_content": "Folytatás",
"refuse_nsfw_content": "Vissza",
"stats": "Statisztikák",
"download_count": "Letöltések",
"player_count": "Aktív játékosok",
"rating_count": "Értékelés",
"download_error": "Ez a letöltési opció nem elérhető",
"download": "Letöltés",
"executable_path_in_use": "Ez a futtatható fájl már használatban van a(z) \"{{game}}\" által",
@@ -192,6 +220,39 @@
"hydra_needs_to_remain_open": "ehhez a letöltéshez, a Hydrának muszáj nyitva maradnia hogy letöltődjön. Ha a Hydra bezáródik letöltés előtt, a letöltés elveszik.",
"achievements": "Achievementek",
"achievements_count": "Achievementek {{unlockedCount}}/{{achievementsCount}}",
"show_more": "Mutass többet",
"show_less": "Mutass kevesebbet",
"reviews": "Vélemények",
"leave_a_review": "Hagyd itt a véleményed",
"write_review_placeholder": "Oszd meg a gondolataid a játékról...",
"sort_newest": "Legújabb",
"no_reviews_yet": "Még nem lett vélemény megosztva",
"be_first_to_review": "Légy az első, aki megossza a véleményét a játékról!",
"sort_oldest": "Legrégibb",
"sort_highest_score": "Legmagasabb Pontszám",
"sort_lowest_score": "Legalacsonyabb Pontszám",
"sort_most_voted": "Legszavazottabb",
"rating": "Értékelés",
"rating_stats": "Értékelés",
"rating_very_negative": "Nagyon Negatív",
"rating_negative": "Negatív",
"rating_neutral": "Átlagos",
"rating_positive": "Pozitív",
"rating_very_positive": "Nagyon Pozitív",
"submit_review": "Küldés",
"submitting": "Küldés alatt...",
"review_submitted_successfully": "Vélemény beküldve sikeresen!",
"review_submission_failed": "Vélemény beküldése sikertelen. Kérlek próbáld újra.",
"review_cannot_be_empty": "A vélemény mező nem lehet üres.",
"review_deleted_successfully": "Vélemény sikeresen törölve.",
"review_deletion_failed": "Vélemény törlése sikertelen. Kérlek próbáld újra.",
"loading_reviews": "Vélemények betöltése...",
"loading_more_reviews": "Több vélemény betöltése...",
"load_more_reviews": "Több vélemény betöltése",
"you_seemed_to_enjoy_this_game": "Úgy látszik élvezted ezt a játékot",
"would_you_recommend_this_game": "Szeretnél véleményt írni erről a játékról?",
"yes": "Igen",
"maybe_later": "Talán Később",
"cloud_save": "Mentés felhőben",
"cloud_save_description": "Mentsd el az előrehaladásod a felhőben, majd folytasd egy másik eszközön",
"backups": "Biztonsági másolatok",
@@ -204,6 +265,7 @@
"uploading_backup": "Biztonsági mentés feltöltése…",
"no_backups": "Még nem hoztál létre biztonsági másolatot ehhez a játékhoz",
"backup_uploaded": "Biztonsági mentés feltöltve",
"backup_failed": "Biztonsági mentés sikertelen",
"backup_deleted": "Biztonsági mentés törölve",
"backup_restored": "Biztonsági mentés helyreállítva",
"see_all_achievements": "Achievementlista megtekintése",
@@ -239,17 +301,17 @@
"reset_achievements_error": "Achievementek nullázása sikertelen",
"download_error_gofile_quota_exceeded": "Túllépted a Gofile havi kvótáját. Kérlek, várd meg amíg a kvóta lejár.",
"download_error_real_debrid_account_not_authorized": "A Real-Debrid fiókod nem jogosult új letöltésekre. Kérlek, ellenőrízd a fiókbeállításaidat, majd próbáld újra.",
"download_error_not_cached_on_real_debrid": "Ez a letöltés nem érhető el a Real-Debridnél, és lekérdezni letöltési állapotot még nem lehet vele.",
"download_error_not_cached_on_real_debrid": "Ez a letöltés nem elérhető a Real-Debriden, és lekérdezni letöltési állapotot még nem lehet.",
"update_playtime_title": "Játékidő frissítése",
"update_playtime_description": "Manuálisan frissíteni a Játékidőt a {{game}} játékhoz",
"update_playtime_description": "A(z) {{game}} játékidejének frissítése manuálisan",
"update_playtime": "Játékidő frissítése",
"update_playtime_success": "Játékidő sikeresen frissítve",
"update_playtime_error": "A Játékidőnek nem sikerült frissülnie",
"update_game_playtime": "Játékidő frissítése",
"manual_playtime_warning": "Az óráid 'manuálisan frissítve' lesznek megjelölve, és ez nem visszavonható.",
"manual_playtime_tooltip": "Ez a játékidő manuálisan lett frissítve",
"download_error_not_cached_on_torbox": "This download is not available on TorBox and polling download status from TorBox is not yet available.",
"download_error_not_cached_on_hydra": "This download is not available on Nimbus.",
"manual_playtime_warning": "A Játékidő „Manuálisan frissített”-ként lesz megjelölve, és ez nem visszavonható.",
"manual_playtime_tooltip": "Ez a Játékidő manuálisan lett frissítve",
"download_error_not_cached_on_torbox": "Ez a letöltés nem elérhető a TorBoxon, és lekérdezni letöltési állapotot még nem lehet.",
"download_error_not_cached_on_hydra": "Ez a letöltés nem elérhető a Nimbuson.",
"game_removed_from_favorites": "Játék eltávolítva a kedvencek közül",
"game_added_to_favorites": "Játék hozzáadva a kedvencekhez",
"game_removed_from_pinned": "Játék eltávolítva a kitűzöttek közül",
@@ -272,7 +334,7 @@
"backup_frozen": "Biztonsági mentés rögzítve",
"backup_unfrozen": "Biztonsági mentés leválasztva",
"backup_freeze_failed": "Biztonsági mentés rögzítése sikertelen",
"backup_freeze_failed_description": "Legalább egy szabad helyet kell hagyni az automatikus biztonsági mentéseknek.",
"backup_freeze_failed_description": "Legalább egy szabad helyet kell hagyni az automatikus biztonsági mentéseknek",
"edit_game_modal_button": "Játékadatok testreszabása",
"game_details": "Játék leírása",
"currency_symbol": "Ft",
@@ -286,13 +348,21 @@
"historical_keyshop": "Korábbi nem hivatalos ár",
"language": "Nyelv",
"caption": "Felirat",
"audio": "Hang"
"audio": "Hang",
"filter_by_source": "Szűrés forrás szerint",
"no_repacks_found": "Nem található forrás ehhez a játékhoz",
"delete_review": "Vélemény törlése",
"remove_review": "Vélemény eltávolítása",
"delete_review_modal_title": "Biztos vagy abban hogy törölni szeretnéd a véleményed?",
"delete_review_modal_description": "Ez a lépés nem vonható vissza.",
"delete_review_modal_delete_button": "Törlés",
"delete_review_modal_cancel_button": "Mégse"
},
"activation": {
"title": "Hydra aktiválása",
"installation_id": "Telepítési azonosító:",
"title": "Hydra Aktiválása",
"installation_id": "Telepítési Azonosító:",
"enter_activation_code": "Írd be az aktiválási kódod",
"message": "Ha nem tudod kit kérdezz efelől, akkor nem kéne nálad legyen.",
"message": "Ha nem tudod hol kérdezz efelől, akkor nem kéne ilyened legyen.",
"activate": "Aktiválás",
"loading": "Töltés…"
},
@@ -324,6 +394,7 @@
"stop_seeding": "Seedelés leállítása",
"resume_seeding": "Seedelés folytatása",
"options": "Kezelés",
"alldebrid_size_not_supported": "Letöltési információ az AllDebrid-hez még nem támogatott",
"extract": "Fájlok kibontása",
"extracting": "Fájlok kibontása…"
},
@@ -342,10 +413,10 @@
"language": "Nyelv",
"api_token": "API Token",
"enable_real_debrid": "Real-Debrid Bekapcsolása",
"real_debrid_description": "A Real-Debrid egy korlátozásmentes letöltőprogram, lehetővé teszi a fájlok gyors letöltését, és csak az internetkapcsolat sebessége szab határt.",
"real_debrid_description": "A Real-Debrid egy korlátozásmentes letöltőprogram, ami lehetővé teszi a fájlok gyors letöltését, és csak az internetkapcsolat sebessége szab határt.",
"debrid_invalid_token": "Érvénytelen API token",
"debrid_api_token_hint": "Az API tokened <0>itt</0> található",
"real_debrid_free_account_error": "Ez a fiók: \"{{username}}\" egy ingyenes fiók. Kérlek iratkozz fel a Real-Debrid-re",
"real_debrid_free_account_error": "Ez a fiók: \"{{username}}\" egy ingyenes fiók. Kérlek iratkozz fel a Real-Debridre",
"debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ",
"save_changes": "Változtatások mentése",
"changes_saved": "Változtatások sikeresen mentve",
@@ -407,7 +478,7 @@
"subscription_renew_cancelled": "Automatikus megújítás kikapcsolva",
"subscription_renews_on": "Az előfizetésed megújul, ekkor: {{date}}",
"bill_sent_until": "A következő számlát ezen napon küldjük",
"no_themes": "Úgy látom nincs egyetlen témád sem még, de ne aggódj, kattints ide hogy elkészítsd a remekművedet.",
"no_themes": "Úgy látszik nincs egyetlen témád sem még, de ne aggódj, kattints ide hogy elkészítsd a remekművedet.",
"editor_tab_code": "Code",
"editor_tab_info": "Info",
"editor_tab_save": "Mentés",
@@ -427,12 +498,25 @@
"delete_theme_description": "Ez törölni fogja a(z) {{theme}} témát",
"cancel": "Mégsem",
"appearance": "Megjelenés",
"debrid": "Debrid",
"debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, csak az internet sebességed szab határt.",
"enable_torbox": "TorBox bekapcsolása",
"torbox_description": "A TorBox egy olyan premium seedbox szolgáltatás, amely még a piacon elérhető legjobb szerverekkel is felveszi a versenyt.",
"torbox_account_linked": "TorBox fiók összekapcsolva",
"create_real_debrid_account": "Kattints ide ha még nincs Real-Debrid fiókod",
"create_torbox_account": "Kattints ide ha még nincs TorBox fiókod",
"real_debrid_account_linked": "Real-Debrid fiók összekapcsolva",
"enable_all_debrid": "All-Debrid bekapcsolása",
"all_debrid_description": "Az All-Debrid egy korlátozásmentes letöltőprogram, ami lehetővé teszi a fájlok gyors letöltését különböző forrásokból.",
"all_debrid_free_account_error": "Ez a fiók: \"{{username}}\" egy ingyenes fiók. Kérlek iratkozz fel az All-Debridre",
"all_debrid_account_linked": "All-Debrid fiók összekapcsolva",
"alldebrid_missing_key": "Kérlek adj meg egy API key-t",
"alldebrid_invalid_key": "Érvénytelen API key",
"alldebrid_blocked": "Az API key-ed Földrajzilag vagy IP-alapján van blokkolva",
"alldebrid_banned": "Ez a fiók kitiltásra került",
"alldebrid_unknown_error": "Egy ismeretlen hiba történt",
"alldebrid_invalid_response": "Érvénytelen válasz az All-Debrid felől",
"alldebrid_network_error": "Hálózati hiba. Ellenőrízd az internetkapcsolatod",
"name_min_length": "A téma neve legalább 3 karakter hosszú legyen",
"import_theme": "Téma importálása",
"import_theme_description": "Ezt a témát fogod importálni a Témaáruház-ból: {{theme}}",
@@ -459,7 +543,7 @@
"variation": "Variáció",
"default": "Alapértelmezett",
"rare": "Ritka",
"platinum": "Platinum",
"platinum": "Platina",
"hidden": "Rejtett",
"test_notification": "Értesítés tesztelése",
"notification_preview": "Achievement Értesítés Előnézete",
@@ -490,12 +574,13 @@
"game_card": {
"available_one": "Elérhető",
"available_other": "Elérhető",
"no_downloads": "Nincs elérhető letöltés"
"no_downloads": "Nincs elérhető letöltés",
"calculating": "Feldolgozás"
},
"binary_not_found_modal": {
"title": "A programok nincsenek telepítve",
"description": "Wine vagy Lutris futtatható fájlok nem találhatók a rendszereden",
"instructions": "Ellenőrízd, hogy melyiket kell helyesen telepíteni a Linux disztribúcióra, hogy a játék normálisan fusson"
"instructions": "Ellenőrízd hogy melyiket kell helyesen telepíteni a Linux disztribúciódra, hogy a játék megfelelően fusson"
},
"modal": {
"close": "Bezárás gomb"
@@ -575,7 +660,7 @@
"report_reason_violence": "Fenyegető",
"report_reason_spam": "Spam",
"report_reason_other": "Egyéb",
"profile_reported": "Profil jelentve",
"profile_reported": "Profil bejelentve",
"your_friend_code": "A barát kódod:",
"upload_banner": "Borítókép feltöltés",
"uploading_banner": "Borítókép feltöltése…",
@@ -593,17 +678,20 @@
"error_adding_friend": "Hiba, barátfelkérés sikertelen. Kérlek ellenőrízd a barát kódot",
"friend_code_length_error": "A barát kódnak 8 karakterből kell állnia",
"game_removed_from_pinned": "Játék eltávolítva a kitűzöttek közül",
"game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez"
"game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez",
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Pozitív értékelésekre kapott pontok alapján"
},
"achievement": {
"achievement_unlocked": "Achievement feloldva",
"user_achievements": "{{displayName}} Achievementjei",
"your_achievements": "A te Achievementjeid",
"unlocked_at": "Feloldva ekkor: {{date}}",
"user_achievements": "{{displayName}} achievementjei",
"your_achievements": "A te achievementjeid",
"unlocked_at": "Feloldva: {{date}}",
"subscription_needed": "A tartalom megtekintéséhez Hydra Cloud előfizetés szükséges",
"new_achievements_unlocked": "{{achievementCount}} új achievementet oldottál fel {{gameCount}} játékban",
"new_achievements_unlocked": "{{achievementCount}} új achievement feloldva {{gameCount}} játékban",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementek",
"achievements_unlocked_for_game": "{{achievementCount}} új achievementet oldottál fel a(z) {{gameTitle}} játékban",
"achievements_unlocked_for_game": "{{achievementCount}} új achievement feloldva itt: {{gameTitle}}",
"hidden_achievement_tooltip": "Ez egy rejtett achievement",
"achievement_earn_points": "Szerezz be {{points}} pontot ezzel az achievement-el",
"earned_points": "Megszerzett pontok:",

View File

@@ -1,6 +1,6 @@
import { Downloader } from "@shared";
export const VERSION_CODENAME = "Lumen";
export const VERSION_CODENAME = "Supernova";
export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid",

View File

@@ -152,6 +152,7 @@ export function GameDetailsContextProvider({
Promise.all([shopDetailsPromise, assetsPromise])
.then(([_, assets]) => {
if (assets) {
if (abortController.signal.aborted) return;
setShopDetails((prev) => {
if (!prev) return null;
return {

View File

@@ -1,4 +1,11 @@
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
PencilIcon,
TrashIcon,
@@ -164,6 +171,8 @@ export function GameDetailsContent() {
const [hasUserReviewed, setHasUserReviewed] = useState(false);
const [reviewCheckLoading, setReviewCheckLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
// Check if the current game is in the user's library
const isGameInLibrary = useMemo(() => {
if (!library || !shop || !objectId) return false;
@@ -225,6 +234,14 @@ export function GameDetailsContent() {
useEffect(() => {
setBackdropOpacity(1);
// Cleanup: abort any pending review requests when objectId changes
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
};
}, [objectId]);
const handleCloudSaveButtonClick = () => {
@@ -256,7 +273,7 @@ export function GameDetailsContent() {
const isCustomGame = game?.shop === "custom";
const checkUserReview = async () => {
const checkUserReview = useCallback(async () => {
if (!objectId || !userDetails) return;
setReviewCheckLoading(true);
@@ -265,51 +282,77 @@ export function GameDetailsContent() {
const hasReviewed = (response as any)?.hasReviewed || false;
setHasUserReviewed(hasReviewed);
const twoHoursInMilliseconds = 2 * 60 * 60 * 1000;
const hasEnoughPlaytime =
game &&
game.playTimeInMilliseconds >= twoHoursInMilliseconds &&
!game.hasManuallyUpdatedPlaytime;
if (
!hasReviewed &&
hasEnoughPlaytime &&
!sessionStorage.getItem(`reviewPromptDismissed_${objectId}`)
) {
setShowReviewPrompt(true);
setShowReviewForm(true);
}
} catch (error) {
console.error("Failed to check user review:", error);
} finally {
setReviewCheckLoading(false);
}
};
}, [objectId, userDetails, shop, game]);
const loadReviews = async (reset = false) => {
if (!objectId) return;
const loadReviews = useCallback(
async (reset = false) => {
if (!objectId) return;
setReviewsLoading(true);
try {
const skip = reset ? 0 : reviewsPage * 20;
const response = await window.electron.getGameReviews(
shop,
objectId,
20,
skip,
reviewsSortBy
);
const reviewsData = (response as any)?.reviews || [];
const reviewCount = (response as any)?.totalCount || 0;
if (reset) {
setReviews(reviewsData);
setReviewsPage(0);
setTotalReviewCount(reviewCount);
} else {
setReviews((prev) => [...prev, ...reviewsData]);
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
setHasMoreReviews(reviewsData.length === 20);
} catch (error) {
console.error("Failed to load reviews:", error);
} finally {
setReviewsLoading(false);
}
};
const abortController = new AbortController();
abortControllerRef.current = abortController;
setReviewsLoading(true);
try {
const skip = reset ? 0 : reviewsPage * 20;
const response = await window.electron.getGameReviews(
shop,
objectId,
20,
skip,
reviewsSortBy
);
if (abortController.signal.aborted) {
return;
}
const reviewsData = (response as any)?.reviews || [];
const reviewCount = (response as any)?.totalCount || 0;
if (reset) {
setReviews(reviewsData);
setReviewsPage(0);
setTotalReviewCount(reviewCount);
} else {
setReviews((prev) => [...prev, ...reviewsData]);
}
setHasMoreReviews(reviewsData.length === 20);
} catch (error) {
if (!abortController.signal.aborted) {
console.error("Failed to load reviews:", error);
}
} finally {
if (!abortController.signal.aborted) {
setReviewsLoading(false);
}
}
},
[objectId, shop, reviewsPage, reviewsSortBy]
);
const handleVoteReview = async (
reviewId: string,
@@ -396,7 +439,6 @@ export function GameDetailsContent() {
const handleReviewPromptYes = () => {
setShowReviewPrompt(false);
setShowReviewForm(true);
setTimeout(() => {
const reviewFormElement = document.querySelector(
@@ -413,6 +455,7 @@ export function GameDetailsContent() {
const handleReviewPromptLater = () => {
setShowReviewPrompt(false);
setShowReviewForm(false);
if (objectId) {
sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, "true");
}
@@ -451,13 +494,13 @@ export function GameDetailsContent() {
loadReviews(true);
checkUserReview();
}
}, [game, shop, objectId, reviewsSortBy, userDetails]);
}, [game, shop, objectId, loadReviews, checkUserReview]);
useEffect(() => {
if (reviewsPage > 0) {
loadReviews(false);
}
}, [reviewsPage]);
}, [reviewsPage, loadReviews]);
// Initialize previousVotesRef for new reviews
useEffect(() => {
@@ -773,216 +816,234 @@ export function GameDetailsContent() {
</div>
)}
{reviews.map((review) => (
<div key={review.id} className="game-details__review-item">
{review.isBlocked &&
!visibleBlockedReviews.has(review.id) ? (
<div className="game-details__blocked-review-simple">
Review from blocked user {" "}
<button
className="game-details__blocked-review-show-link"
onClick={() => toggleBlockedReview(review.id)}
>
Show
</button>
</div>
) : (
<>
<div className="game-details__review-header">
<div className="game-details__review-user">
{review.user?.profileImageUrl && (
<button
className="game-details__review-avatar-button"
onClick={() =>
review.user?.id &&
navigate(`/profile/${review.user.id}`)
}
title={review.user.displayName || "User"}
>
<img
src={review.user.profileImageUrl}
alt={review.user.displayName || "User"}
className="game-details__review-avatar"
/>
</button>
)}
<div className="game-details__review-user-info">
<button
className="game-details__review-display-name game-details__review-display-name--clickable"
onClick={() =>
review.user?.id &&
navigate(`/profile/${review.user.id}`)
}
>
{review.user?.displayName || "Anonymous"}
</button>
<div className="game-details__review-date">
<ClockIcon size={12} />
{formatDistance(
new Date(review.createdAt),
new Date(),
{ addSuffix: true }
)}
<div
style={{
opacity: reviewsLoading && reviews.length > 0 ? 0.5 : 1,
transition: "opacity 0.2s ease",
}}
>
{reviews.map((review) => (
<div
key={review.id}
className="game-details__review-item"
>
{review.isBlocked &&
!visibleBlockedReviews.has(review.id) ? (
<div className="game-details__blocked-review-simple">
Review from blocked user {" "}
<button
className="game-details__blocked-review-show-link"
onClick={() => toggleBlockedReview(review.id)}
>
Show
</button>
</div>
) : (
<>
<div className="game-details__review-header">
<div className="game-details__review-user">
{review.user?.profileImageUrl && (
<button
className="game-details__review-avatar-button"
onClick={() =>
review.user?.id &&
navigate(`/profile/${review.user.id}`)
}
title={review.user.displayName || "User"}
>
<img
src={review.user.profileImageUrl}
alt={review.user.displayName || "User"}
className="game-details__review-avatar"
/>
</button>
)}
<div className="game-details__review-user-info">
<button
className="game-details__review-display-name game-details__review-display-name--clickable"
onClick={() =>
review.user?.id &&
navigate(`/profile/${review.user.id}`)
}
>
{review.user?.displayName || "Anonymous"}
</button>
<div className="game-details__review-date">
<ClockIcon size={12} />
{formatDistance(
new Date(review.createdAt),
new Date(),
{ addSuffix: true }
)}
</div>
</div>
</div>
<div
className="game-details__review-score-stars"
title={getRatingText(review.score, t)}
>
{[1, 2, 3, 4, 5].map((starValue) => (
<Star
key={starValue}
size={20}
fill={
starValue <= review.score
? "currentColor"
: "none"
}
className={`game-details__review-star ${
starValue <= review.score
? "game-details__review-star--filled"
: "game-details__review-star--empty"
} ${
starValue <= review.score
? getScoreColorClass(review.score)
: ""
}`}
/>
))}
</div>
</div>
<div
className="game-details__review-score-stars"
title={getRatingText(review.score, t)}
>
{[1, 2, 3, 4, 5].map((starValue) => (
<Star
key={starValue}
size={20}
fill={
starValue <= review.score
? "currentColor"
: "none"
className="game-details__review-content"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(review.reviewHtml),
}}
/>
<div className="game-details__review-actions">
<div className="game-details__review-votes">
<motion.button
className={`game-details__vote-button game-details__vote-button--upvote ${review.hasUpvoted ? "game-details__vote-button--active" : ""}`}
onClick={() =>
handleVoteReview(review.id, "upvote")
}
animate={
review.hasUpvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
}
className={`game-details__review-star ${
starValue <= review.score
? "game-details__review-star--filled"
: "game-details__review-star--empty"
} ${
starValue <= review.score
? getScoreColorClass(review.score)
: ""
}`}
/>
))}
</div>
</div>
<div
className="game-details__review-content"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(review.reviewHtml),
}}
/>
<div className="game-details__review-actions">
<div className="game-details__review-votes">
<motion.button
className={`game-details__vote-button game-details__vote-button--upvote ${review.hasUpvoted ? "game-details__vote-button--active" : ""}`}
onClick={() =>
handleVoteReview(review.id, "upvote")
}
animate={
review.hasUpvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
}
>
<ThumbsUp size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.upvotes || 0}
custom={
(review.upvotes || 0) >
(previousVotesRef.current.get(review.id)
?.upvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(review.id, {
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
});
}}
>
{formatNumber(review.upvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
<motion.button
className={`game-details__vote-button game-details__vote-button--downvote ${review.hasDownvoted ? "game-details__vote-button--active" : ""}`}
onClick={() =>
handleVoteReview(review.id, "downvote")
}
animate={
review.hasDownvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
}
>
<ThumbsDown size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.downvotes || 0}
custom={
(review.downvotes || 0) >
(previousVotesRef.current.get(review.id)
?.downvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(review.id, {
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
});
}}
>
{formatNumber(review.downvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
</div>
{userDetails?.id === review.user?.id && (
<button
className="game-details__delete-review-button"
onClick={() => handleDeleteReview(review.id)}
title={t("delete_review")}
>
<TrashIcon size={16} />
<span>{t("remove_review")}</span>
</button>
)}
{review.isBlocked &&
visibleBlockedReviews.has(review.id) && (
<button
className="game-details__blocked-review-hide-link"
onClick={() => toggleBlockedReview(review.id)}
>
Hide
<ThumbsUp size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.upvotes || 0}
custom={
(review.upvotes || 0) >
(previousVotesRef.current.get(review.id)
?.upvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(
review.id,
{
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
}
);
}}
>
{formatNumber(review.upvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
<motion.button
className={`game-details__vote-button game-details__vote-button--downvote ${review.hasDownvoted ? "game-details__vote-button--active" : ""}`}
onClick={() =>
handleVoteReview(review.id, "downvote")
}
animate={
review.hasDownvoted
? {
scale: [1, 1.2, 1],
transition: { duration: 0.3 },
}
: {}
}
>
<ThumbsDown size={16} />
<AnimatePresence mode="wait">
<motion.span
key={review.downvotes || 0}
custom={
(review.downvotes || 0) >
(previousVotesRef.current.get(review.id)
?.downvotes || 0)
}
variants={{
enter: (isIncreasing: boolean) => ({
y: isIncreasing ? 10 : -10,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (isIncreasing: boolean) => ({
y: isIncreasing ? -10 : 10,
opacity: 0,
}),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2 }}
onAnimationComplete={() => {
previousVotesRef.current.set(
review.id,
{
upvotes: review.upvotes || 0,
downvotes: review.downvotes || 0,
}
);
}}
>
{formatNumber(review.downvotes || 0)}
</motion.span>
</AnimatePresence>
</motion.button>
</div>
{userDetails?.id === review.user?.id && (
<button
className="game-details__delete-review-button"
onClick={() => handleDeleteReview(review.id)}
title={t("delete_review")}
>
<TrashIcon size={16} />
<span>{t("remove_review")}</span>
</button>
)}
</div>
</>
)}
</div>
))}
{review.isBlocked &&
visibleBlockedReviews.has(review.id) && (
<button
className="game-details__blocked-review-hide-link"
onClick={() =>
toggleBlockedReview(review.id)
}
>
Hide
</button>
)}
</div>
</>
)}
</div>
))}
</div>
{hasMoreReviews && !reviewsLoading && (
<button

View File

@@ -67,6 +67,12 @@ export function EditGameModal({
};
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
// Check if assets were removed (URLs are null but original paths exist)
const iconRemoved = !game.iconUrl && (game as any).originalIconPath;
const logoRemoved = !game.logoImageUrl && (game as any).originalLogoPath;
const heroRemoved =
!game.libraryHeroImageUrl && (game as any).originalHeroPath;
setAssetPaths({
icon: extractLocalPath(game.iconUrl),
logo: extractLocalPath(game.logoImageUrl),
@@ -85,10 +91,25 @@ export function EditGameModal({
(game as any).originalHeroPath ||
extractLocalPath(game.libraryHeroImageUrl),
});
// Set removed assets state based on whether assets were explicitly removed
setRemovedAssets({
icon: iconRemoved,
logo: logoRemoved,
hero: heroRemoved,
});
}, []);
const setNonCustomGameAssets = useCallback(
(game: LibraryGame) => {
// Check if assets were removed (custom URLs are null but original paths exist)
const iconRemoved =
!game.customIconUrl && (game as any).customOriginalIconPath;
const logoRemoved =
!game.customLogoImageUrl && (game as any).customOriginalLogoPath;
const heroRemoved =
!game.customHeroImageUrl && (game as any).customOriginalHeroPath;
setAssetPaths({
icon: extractLocalPath(game.customIconUrl),
logo: extractLocalPath(game.customLogoImageUrl),
@@ -111,6 +132,13 @@ export function EditGameModal({
extractLocalPath(game.customHeroImageUrl),
});
// Set removed assets state based on whether assets were explicitly removed
setRemovedAssets({
icon: iconRemoved,
logo: logoRemoved,
hero: heroRemoved,
});
setDefaultUrls({
icon: shopDetails?.assets?.iconUrl || game.iconUrl || null,
logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null,
@@ -148,8 +176,12 @@ export function EditGameModal({
};
const getAssetDisplayPath = (assetType: AssetType): string => {
// Use original path if available, otherwise fall back to display path
return originalAssetPaths[assetType] || assetDisplayPaths[assetType];
// If asset was removed, don't show any path
if (removedAssets[assetType]) {
return "";
}
// Use display path first, then fall back to original path
return assetDisplayPaths[assetType] || originalAssetPaths[assetType];
};
const setAssetPath = (assetType: AssetType, path: string): void => {
@@ -221,18 +253,11 @@ export function EditGameModal({
};
const handleRestoreDefault = (assetType: AssetType) => {
if (game && isCustomGame(game)) {
// For custom games, mark asset as removed and clear paths
setRemovedAssets((prev) => ({ ...prev, [assetType]: true }));
setAssetPath(assetType, "");
setAssetDisplayPath(assetType, "");
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
} else {
// For non-custom games, clear custom assets (restore to shop defaults)
setAssetPath(assetType, "");
setAssetDisplayPath(assetType, "");
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
}
// Mark asset as removed and clear paths (for both custom and non-custom games)
setRemovedAssets((prev) => ({ ...prev, [assetType]: true }));
setAssetPath(assetType, "");
setAssetDisplayPath(assetType, "");
// Don't clear originalAssetPaths - keep them for reference but don't use them for display
};
const getOriginalTitle = (): string => {
@@ -402,10 +427,28 @@ export function EditGameModal({
// Helper function to prepare non-custom game assets
const prepareNonCustomGameAssets = () => {
const hasIconPath = assetPaths.icon;
let customIconUrl: string | null = null;
if (!removedAssets.icon && hasIconPath) {
customIconUrl = `local:${assetPaths.icon}`;
}
const hasLogoPath = assetPaths.logo;
let customLogoImageUrl: string | null = null;
if (!removedAssets.logo && hasLogoPath) {
customLogoImageUrl = `local:${assetPaths.logo}`;
}
const hasHeroPath = assetPaths.hero;
let customHeroImageUrl: string | null = null;
if (!removedAssets.hero && hasHeroPath) {
customHeroImageUrl = `local:${assetPaths.hero}`;
}
return {
customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null,
customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null,
customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null,
customIconUrl,
customLogoImageUrl,
customHeroImageUrl,
};
};
@@ -439,9 +482,15 @@ export function EditGameModal({
customIconUrl,
customLogoImageUrl,
customHeroImageUrl,
customOriginalIconPath: originalAssetPaths.icon || undefined,
customOriginalLogoPath: originalAssetPaths.logo || undefined,
customOriginalHeroPath: originalAssetPaths.hero || undefined,
customOriginalIconPath: removedAssets.icon
? undefined
: originalAssetPaths.icon || undefined,
customOriginalLogoPath: removedAssets.logo
? undefined
: originalAssetPaths.logo || undefined,
customOriginalHeroPath: removedAssets.hero
? undefined
: originalAssetPaths.hero || undefined,
});
};
@@ -484,6 +533,23 @@ export function EditGameModal({
hero: false,
});
// Clear all asset paths to ensure clean state
setAssetPaths({
icon: "",
logo: "",
hero: "",
});
setAssetDisplayPaths({
icon: "",
logo: "",
hero: "",
});
setOriginalAssetPaths({
icon: "",
logo: "",
hero: "",
});
if (isCustomGame(game)) {
setCustomGameAssets(game);
// Clear default URLs for custom games