Merge branch 'main' into feat/context_game_menu

This commit is contained in:
Carlos Eduardo Mariano Garcia Pereira
2025-10-01 00:34:48 -03:00
committed by GitHub
59 changed files with 1698 additions and 637 deletions

View File

@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
os: [windows-2022, ubuntu-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
@@ -58,7 +58,7 @@ jobs:
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Build Windows
if: matrix.os == 'windows-latest'
if: matrix.os == 'windows-2022'
run: yarn build:win
env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}

View File

@@ -12,7 +12,7 @@ jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
os: [windows-2022, ubuntu-latest]
runs-on: ${{ matrix.os }}
@@ -59,7 +59,7 @@ jobs:
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Build Windows
if: matrix.os == 'windows-latest'
if: matrix.os == 'windows-2022'
run: yarn build:win
env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
},
"home": {
"featured": "مميز",
"surprise_me": "مفاجئني",
"no_results": "لم يتم العثور على نتائج",
"start_typing": "ابدأ بالكتابة للبحث...",

View File

@@ -1,7 +1,6 @@
{
"language_name": "беларуская мова",
"home": {
"featured": "Рэкамэндаванае",
"surprise_me": "Здзіві мяне",
"no_results": "Няма вынікаў"
},
@@ -17,7 +16,6 @@
"home": "Галоўная",
"favorites": "Улюбленыя"
},
"header": {
"search": "Пошук",
"home": "Галоўная",
@@ -31,10 +29,7 @@
"downloading_metadata": "Сцягванне мэтаданых {{title}}…",
"downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Наступная старонка",
"previous_page": "Папярэдняя старонка"
},
"catalogue": {},
"game_details": {
"open_download_options": "Адкрыць варыянты сцягвання",
"download_options_zero": "Няма варыянтаў сцягвання",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Успешно влизане"
},
"home": {
"featured": "Препоръчани",
"surprise_me": "Изненадай ме",
"no_results": "Няма намерени резултати",
"start_typing": "Започнете да пишете за търсене...",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Has entrat correctament"
},
"home": {
"featured": "Destacats",
"surprise_me": "Sorprèn-me",
"no_results": "No s'ha trobat res"
},
@@ -25,7 +24,6 @@
},
"header": {
"search": "Cerca jocs",
"home": "Inici",
"catalogue": "Catàleg",
"downloads": "Baixades",
@@ -41,10 +39,7 @@
"calculating_eta": "Descarregant {{title}}… ({{percentage}} completat) - Calculant el temps restant…",
"checking_files": "Comprovant els fitxers de {{title}}… ({{percentage}} completat)"
},
"catalogue": {
"next_page": "Pàgina següent",
"previous_page": "Pàgina anterior"
},
"catalogue": {},
"game_details": {
"open_download_options": "Obre les opcions de baixada",
"download_options_zero": "No hi ha opcions de baixada",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Úspěšně přihlášen"
},
"home": {
"featured": "Doporučené",
"surprise_me": "Překvap mě",
"no_results": "Výsledek nenalezen",
"start_typing": "Začni psát pro vyhledávání...",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Loggede ind successfuldt"
},
"home": {
"featured": "Anbefalet",
"surprise_me": "Overrask mig",
"no_results": "Ingen resultater fundet",
"start_typing": "Begynd at skrive for at søge...",
@@ -29,7 +28,6 @@
},
"header": {
"search": "Søg efter spil",
"home": "Hjem",
"catalogue": "Katalog",
"downloads": "Downloads",
@@ -45,10 +43,7 @@
"calculating_eta": "Downloader {{title}}… ({{percentage}} færdig) - Udregner resterende tid…",
"checking_files": "Checker {{title}} filer… ({{percentage}} færdig)"
},
"catalogue": {
"next_page": "Næste side",
"previous_page": "Forrige side"
},
"catalogue": {},
"game_details": {
"open_download_options": "Åben download muligheder",
"download_options_zero": "Ingen download mulighed",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Erfolgreich angemeldet"
},
"home": {
"featured": "Empfohlen",
"surprise_me": "Überrasche mich",
"no_results": "Keine Ergebnisse gefunden",
"start_typing": "Tippe, um zu suchen...",
@@ -59,9 +58,7 @@
"download_sources": "Download-Quellen",
"result_count": "{{resultCount}} Ergebnisse",
"filter_count": "{{filterCount}} verfügbar",
"clear_filters": "{{filterCount}} ausgewählte löschen",
"next_page": "Nächste Seite",
"previous_page": "Vorherige Seite"
"clear_filters": "{{filterCount}} ausgewählte löschen"
},
"game_details": {
"open_download_options": "Download-Optionen öffnen",

View File

@@ -70,7 +70,13 @@
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
"edit_game_modal_assets": "Assets"
"edit_game_modal_assets": "Assets",
"edit_game_modal_drop_icon_image_here": "Drop icon image here",
"edit_game_modal_drop_logo_image_here": "Drop logo image here",
"edit_game_modal_drop_hero_image_here": "Drop hero image here",
"edit_game_modal_drop_to_replace_icon": "Drop to replace icon",
"edit_game_modal_drop_to_replace_logo": "Drop to replace logo",
"edit_game_modal_drop_to_replace_hero": "Drop to replace hero"
},
"header": {
"search": "Search games",
@@ -295,7 +301,9 @@
"historical_keyshop": "Historical keyshop",
"language": "Language",
"caption": "Caption",
"audio": "Audio"
"audio": "Audio",
"filter_by_source": "Filter by source",
"no_repacks_found": "No sources found for this game"
},
"activation": {
"title": "Activate Hydra",
@@ -515,6 +523,8 @@
"user_profile": {
"amount_hours": "{{amount}} hours",
"amount_minutes": "{{amount}} minutes",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"last_time_played": "Last played {{period}}",
"activity": "Recent Activity",
"library": "Library",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Edukalt sisse logitud"
},
"home": {
"featured": "Esile toodud",
"surprise_me": "Üllata mind",
"no_results": "Tulemusi ei leitud",
"start_typing": "Alusta otsimiseks kirjutamist...",
@@ -45,10 +44,7 @@
"calculating_eta": "{{title}} allalaadimine… ({{percentage}} valmis) - Järelejäänud aja arvutamine…",
"checking_files": "{{title}} failide kontrollimine… ({{percentage}} valmis)"
},
"catalogue": {
"next_page": "Järgmine leht",
"previous_page": "Eelmine leht"
},
"catalogue": {},
"game_details": {
"open_download_options": "Ava allalaadimise valikud",
"download_options_zero": "Allalaadimise valikuid pole",

View File

@@ -1,7 +1,6 @@
{
"language_name": "فارسی",
"home": {
"featured": "پیشنهادی",
"surprise_me": "سوپرایزم کن",
"no_results": "اتمام‌ای پیدا نشد"
},
@@ -17,7 +16,6 @@
"home": "خانه",
"favorites": "علاقه‌مندی‌ها"
},
"header": {
"search": "جستجوی بازی‌ها",
"home": "خانه",
@@ -31,10 +29,7 @@
"downloading_metadata": "درحال دانلود متادیتاهای {{title}}…",
"downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "صفحه‌ی بعدی",
"previous_page": "صفحه‌ی قبلی"
},
"catalogue": {},
"game_details": {
"open_download_options": "بازکردن آپشن‌های دانلود",
"download_options_zero": "هیچ آپشن دانلودی وجود ندارد",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Connecté avec succès"
},
"home": {
"featured": "En vedette",
"surprise_me": "Surprenez-moi",
"no_results": "Aucun résultat trouvé",
"start_typing": "Commencez à taper pour rechercher...",

View File

@@ -1,45 +1,113 @@
{
"language_name": "Magyar",
"app": {
"successfully_signed_in": "Sikeresen bejelentkeztél"
},
"home": {
"featured": "Featured",
"surprise_me": "Lepj meg",
"no_results": "Nem található"
"no_results": "Nincs találat",
"start_typing": "Kereséshez gépelj...",
"hot": "Most felkapott",
"weekly": "📅 A hét felkapott játékai",
"achievements": "🏆 Achievement támogatott"
},
"sidebar": {
"catalogue": "Katalógus",
"downloads": "Letöltések",
"settings": "Beállítások",
"my_library": "Könyvtáram",
"downloading_metadata": "{{title}} (Metadata letöltése…)",
"paused": "{{title}} (Szünet)",
"downloading_metadata": "{{title}} (metaadatai letöltése…)",
"paused": "{{title}} (Szüneteltetve)",
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
"filter": "Könyvtár szűrése",
"home": "Főoldal",
"favorites": "Kedvenc játékok"
"queued": "A(z) {{title}} (Várakozósorban van)",
"game_has_no_executable": "A játékhoz nincs tallózva futtatható fájl",
"sign_in": "Bejelentkezés",
"friends": "Barátok",
"need_help": "Elakadtál?",
"favorites": "Kedvenc játékok",
"playable_button_title": "Csak az azonnal játszható játékokat mutasd",
"add_custom_game_tooltip": "Saját játék hozzáadása",
"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_title": "Játékcím",
"custom_game_modal_enter_title": "Játék elnevezése",
"custom_game_modal_browse": "Tallózás",
"custom_game_modal_cancel": "Mégse",
"custom_game_modal_add": "Játék hozzáadása",
"custom_game_modal_adding": "Játék hozzáadása...",
"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_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",
"edit_game_modal_image": "Kép",
"edit_game_modal_select_image": "Kép útvonala",
"edit_game_modal_browse": "Tallózás",
"edit_game_modal_image_preview": "Kép előnézete",
"edit_game_modal_icon": "Ikon",
"edit_game_modal_select_icon": "Ikon útvonala",
"edit_game_modal_icon_preview": "Ikon előnézete",
"edit_game_modal_logo": "Logó",
"edit_game_modal_select_logo": "Logó útvonala",
"edit_game_modal_logo_preview": "Logó előnézete",
"edit_game_modal_hero": "Borítókép",
"edit_game_modal_select_hero": "Borítókép útvonala",
"edit_game_modal_hero_preview": "Borítókép előnézete",
"edit_game_modal_cancel": "Mégse",
"edit_game_modal_update": "Frissít",
"edit_game_modal_updating": "Frissítés...",
"edit_game_modal_fill_required": "Kérlek töltsd ki az összes kötelező mezőt",
"edit_game_modal_success": "Játék megjelenés frissítése sikeres",
"edit_game_modal_failed": "Játék megjelenés frissítése sikertelen",
"edit_game_modal_image_filter": "Kép",
"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:"
},
"header": {
"search": "Keresés",
"home": "Főoldal",
"catalogue": "Katalógus",
"downloads": "Letöltések",
"search_results": "Keresési eredmények",
"settings": "Beállítások"
"search_results": "Keresési találatok",
"settings": "Beállítások",
"version_available_install": "A(z) {{version}} verzió elérhető. Kattints ide az újraindításhoz és telepítéshez.",
"version_available_download": "A(z) {{version}} verzió elérhető. A letöltéshez kattints ide."
},
"bottom_panel": {
"no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések",
"no_downloads_in_progress": "Nincs folyamatban lévő letöltés",
"downloading_metadata": "{{title}} metaadatainak letöltése…",
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}",
"calculating_eta": "{{title}} letöltése… ({{percentage}} kész) - Hátralévő idő…",
"checking_files": "A(z) {{title}} fájljaiból… ({{percentage}} kész)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Telepítés befejezve",
"installation_complete_message": "A(z) Alapvető segédprogramok sikeresen telepítve"
},
"catalogue": {
"next_page": "Következő olda",
"previous_page": "Előző olda"
"search": "Szűrés…",
"developers": "Fejlesztők",
"genres": "Műfajok",
"tags": "Címkék",
"publishers": "Kiadók",
"download_sources": "Letöltési források",
"result_count": "{{resultCount}} találatok",
"filter_count": "{{filterCount}} elérhető",
"clear_filters": "{{filterCount}} kiválaszott szűrő törlése"
},
"game_details": {
"open_download_options": "Letöltési lehetőségek",
"download_options_zero": "Nincs letöltési lehetőség",
"download_options_one": "{{count}} letöltési lehetőség",
"download_options_other": "{{count}} letöltési lehetőség",
"open_download_options": "Letöltési opciók megnyitása",
"download_options_zero": "Nincs letöltési opció",
"download_options_one": "{{count}} letöltési opció",
"download_options_other": "{{count}} letöltési opció",
"updated_at": "Frissítve: {{updated_at}}",
"install": "Letöltés",
"resume": "Folytatás",
@@ -48,11 +116,13 @@
"remove": "Eltávolítás",
"space_left_on_disk": "{{space}} szabad hely a lemezen",
"eta": "Befejezés {{eta}}",
"downloading_metadata": "Metaadatok letöltése…",
"calculating_eta": "Hátralevő idő kiszámítása…",
"downloading_metadata": "Metaadat letöltése",
"filter": "Repackek szűrése",
"requirements": "Rendszerkövetelmények",
"minimum": "Minimális",
"minimum": "Minimum",
"recommended": "Ajánlott",
"paused": "Szüneteltetve",
"release_date": "Megjelenés: {{date}}",
"publisher": "Kiadta: {{publisher}}",
"hours": "óra",
@@ -60,29 +130,171 @@
"amount_hours": "{{amount}} óra",
"amount_minutes": "{{amount}} perc",
"accuracy": "{{accuracy}}% pontosság",
"add_to_library": "Hozzáadás a könyvtárhoz",
"add_to_library": "Könyvtárba helyezés",
"already_in_library": "Már könyvtárban",
"remove_from_library": "Eltávolítás a könyvtárból",
"no_downloads": "Nincs elérhető letöltés",
"play_time": "Játszva: {{amount}}",
"last_time_played": "Utoljára játszva {{period}}",
"not_played_yet": "{{title}} még nem játszottál",
"last_time_played": "Utoljára játszva: {{period}}",
"not_played_yet": "Ezzel a játékkal még nem játszottál: {{title}}",
"next_suggestion": "Következő javaslat",
"play": "Játék",
"deleting": "Telepítő törlése…",
"close": "Bezárás",
"playing_now": "Jelenleg játszva",
"playing_now": "Játékban: ",
"change": "Változtatás",
"repacks_modal_description": "Choose the repack you want to download",
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
"download_now": "Töltsd le most"
"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",
"download_now": "Letöltés",
"no_shop_details": "A bolt adatai nem érhetőek el.",
"download_options": "Letöltési opciók",
"download_path": "Letöltis hely",
"previous_screenshot": "Előző screenshot",
"next_screenshot": "Következő screenshot",
"screenshot": "Screenshot {{number}}",
"open_screenshot": "Screenshot megnyitása {{number}}",
"download_settings": "Letöltési beállítások",
"downloader": "Letöltési mód",
"select_executable": "Tallózás",
"no_executable_selected": "Nincs futtatható fájl tallózva",
"open_folder": "Mappa megnyitása",
"open_download_location": "Letöltött fájlok megtekintése",
"create_shortcut": "Asztali 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",
"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",
"download_in_progress": "Letöltés folyamatban",
"download_paused": "Letöltés szüneteltetve",
"last_downloaded_option": "Utoljára letöltött",
"create_steam_shortcut": "Steam parancsikon létrehozása",
"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?",
"allow_nsfw_content": "Folytatás",
"refuse_nsfw_content": "Vissza",
"stats": "Statisztikák",
"download_count": "Letöltések",
"player_count": "Aktív játékosok",
"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",
"warning": "Figyelmeztetés:",
"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}}",
"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",
"install_backup": "Telepít",
"delete_backup": "Töröl",
"create_backup": "Biztonsági másolat létrehozása",
"last_backup_date": "Utolsó biztonsági mentés {{date}}",
"no_backup_preview": "Ehhez a címhez nem található mentett játék",
"restoring_backup": "Biztonsági mentés helyreállítás: ({{progress}} kész)…",
"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_deleted": "Biztonsági mentés törölve",
"backup_restored": "Biztonsági mentés helyreállítva",
"see_all_achievements": "Achievementlista megtekintése",
"sign_in_to_see_achievements": "Jelentkezz be hogy lásd az achievementjeid",
"mapping_method_automatic": "Automatikus",
"mapping_method_manual": "Kézi",
"mapping_method_label": "Térképezési módszer",
"files_automatically_mapped": "Fájlok automatikusan térképezve",
"no_backups_created": "Ehhez a játékhoz nincs biztonsági másolat létrehozva",
"manage_files": "Fájlok kezelése",
"loading_save_preview": "Mentett játék keresése…",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "A Wine környezet, amiben a játék fut",
"launch_options": "Indítási opciók",
"launch_options_description": "Indítási opciók testreszabása haladó felhasználóknak (kísérleti funkció)",
"launch_options_placeholder": "Nincs paraméter megadva",
"no_download_option_info": "Nincs elérhető információ",
"backup_deletion_failed": "Biztonsági mentés törlése sikertelen",
"max_number_of_artifacts_reached": "A játék biztonsági mentéseinek száma elérte a határt",
"achievements_not_sync": "Tekintsd meg hogyan kell szinkronizálni az achievementjeid",
"manage_files_description": "Kezeld mely fájlokról készül biztonsági másolat, és melyek állíthatók vissza",
"select_folder": "Mappa tallózása",
"backup_from": "Biztonsági másolat: {{date}}",
"automatic_backup_from": "Automatikus másolat: {{date}}",
"enable_automatic_cloud_sync": "Automatikus felhőalapú szinkronizálás engedélyezése",
"custom_backup_location_set": "Egyéni biztonsági mentési hely",
"no_directory_selected": "Nincs mappa tallózva",
"no_write_permission": "Nem lehet a mappába letölteni. Kattints ide további információért.",
"reset_achievements": "Achievementek nullázása",
"reset_achievements_description": "Ez az összes achievementet nullázza a {{game}} játékhoz",
"reset_achievements_title": "Biztos vagy ebben?",
"reset_achievements_success": "Achievementek sikeresen nullázva",
"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.",
"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": "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.",
"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",
"game_added_to_pinned": "Játék sikeresen kitűzve",
"automatically_extract_downloaded_files": "Automatikus kibontása a letöltött fájloknak",
"create_start_menu_shortcut": "Start menü parancsikon létrehozása",
"invalid_wine_prefix_path": "Érvénytelen Wine prefix elérési útvonal",
"invalid_wine_prefix_path_description": "Az út a Wine prefixhez érvénytelen. Ellenőrízd az elérési utat, majd próbáld újra.",
"missing_wine_prefix": "Wine prefix szükséges a biztonsági másolat létrehozásához Linux rendszeren.",
"artifact_renamed": "Biztonsági mentés sikeresen átnevezve",
"rename_artifact": "Biztonsági mentés átnevezése",
"rename_artifact_description": "Nevezd át a biztonsági másolatot egy leíróbb névre",
"artifact_name_label": "Biztonsági másolat neve",
"artifact_name_placeholder": "Adj egy nevet a biztonsági mentésnek",
"save_changes": "Változtatások mentése",
"required_field": "Ez a mező kötelező",
"max_length_field": "Ez a mező kevesebb karakter kell legyen mint {{length}}",
"freeze_backup": "Rögzítsd, hogy az automatikus biztonsági mentések ne írják felül",
"unfreeze_backup": "Leválaszt",
"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.",
"edit_game_modal_button": "Játékadatok testreszabása",
"game_details": "Játék leírása",
"currency_symbol": "Ft",
"currency_country": "hu",
"prices": "Árak",
"no_prices_found": "Nincsenek található árak",
"view_all_prices": "Összes ár megtekintése",
"retail_price": "Bolti ár",
"keyshop_price": "Nem hivatalos ár",
"historical_retail": "Korábbi bolti ár",
"historical_keyshop": "Korábbi nem hivatalos ár",
"language": "Nyelv",
"caption": "Felirat",
"audio": "Hang"
},
"activation": {
"title": "Hydra Aktiválása",
"installation_id": "Telepítési ID:",
"enter_activation_code": "Add meg az aktiválási kódodat",
"message": "Ha nem tudod, hol kérdezd meg ezt, akkor nem is kellene, hogy legyen ilyened.",
"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.",
"activate": "Aktiválás",
"loading": "Betöltés…"
"loading": "Töltés…"
},
"downloads": {
"resume": "Folytatás",
@@ -91,46 +303,325 @@
"paused": "Szüneteltetve",
"verifying": "Ellenőrzés…",
"completed": "Befejezve",
"removed": "Nincs letöltve",
"cancel": "Mégse",
"filter": "Letöltött játékok szűrése",
"remove": "Eltávolítás",
"remove": "Eltávolít",
"downloading_metadata": "Metaadatok letöltése…",
"deleting": "Telepítő törlése…",
"delete": "Telepítő eltávolítása",
"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"
"delete_modal_title": "Biztos vagy ebben?",
"delete_modal_description": "Ez eltávolítja a telepítési fájlokat a számítógépedről",
"install": "Telepít",
"download_in_progress": "Folyamatban lévő",
"queued_downloads": "Várakozósoron lévő letöltések",
"downloads_completed": "Befejezett",
"queued": "Várakozási sorban",
"no_downloads_title": "Oly üres..",
"no_downloads_description": "Még nem töltöttél le semmit a Hydra segítségével, de soha nem késő elkezdeni.",
"checking_files": "Fájlok ellenőrzése…",
"seeding": "Seedelés",
"stop_seeding": "Seedelés leállítása",
"resume_seeding": "Seedelés folytatása",
"options": "Kezelés",
"extract": "Fájlok kibontása",
"extracting": "Fájlok kibontása…"
},
"settings": {
"downloads_path": "Letöltések helye",
"downloads_path": "Letöltési útvonalak",
"change": "Frissítés",
"notifications": "Értesítések",
"enable_download_notifications": "Amikor egy letöltés befejeződik",
"enable_repack_list_notifications": "Amikor új repack kerül feltöltésre",
"real_debrid_api_token_label": "Real-Debrid API token",
"quit_app_instead_hiding": "Hydra elrejtésének tiltása bezáráskor",
"launch_with_system": "Hydra automatikus indítása rendszer indításakor",
"general": "Általános",
"behavior": "Működés",
"download_sources": "Letöltési források",
"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.",
"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",
"debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ",
"save_changes": "Változtatások mentése",
"changes_saved": "Változtatások sikeresen mentve",
"download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
"validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása",
"download_count_zero": "Nincs letöltési opció",
"download_count_one": "{{countFormatted}} letöltési opció",
"download_count_other": "{{countFormatted}} letöltési opció",
"download_source_url": "URL forrás:",
"add_download_source_description": "Helyezd be a .json fájl URL-jét",
"download_source_up_to_date": "Naprakész",
"download_source_errored": "Hiba történt",
"sync_download_sources": "Források szinkronizálása",
"removed_download_source": "Letöltési forrás eltávolítva",
"removed_download_sources": "Letöltési források eltávolítva",
"cancel_button_confirmation_delete_all_sources": "Nem",
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
"description_confirmation_delete_all_sources": "Törölni fog minden letöltési forrást",
"title_confirmation_delete_all_sources": "Törölje az összes letöltési forrást",
"removed_download_sources": "Betűtípusok eltávolítva",
"button_delete_all_sources": "Távolítsa el az összes letöltési forrást",
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül"
"title_confirmation_delete_all_sources": "Az összes letöltési forrás törlése",
"description_confirmation_delete_all_sources": "Az összes letöltési forrást törölni fogod ezáltal",
"button_delete_all_sources": "Összes eltávolítása",
"added_download_source": "Letöltési forrás hozzáadva",
"download_sources_synced": "Az összes letöltési forrás szinkronizálva",
"insert_valid_json_url": "Adj meg egy érvényes JSON url-t",
"found_download_option_zero": "Nincs letöltési opció",
"found_download_option_one": "{{countFormatted}} Letöltési opció találva",
"found_download_option_other": "{{countFormatted}} Letöltési opciók találva",
"import": "Importálás",
"public": "Publikus",
"private": "Privát",
"friends_only": "Csak barátok",
"privacy": "Adatvédelem",
"profile_visibility": "Profil láthatósága",
"profile_visibility_description": "Válaszd ki, ki láthatja a profilod és könyvtárad",
"required_field": "Ez a mező kötelező",
"source_already_exists": "Ez a forrás már használatban",
"must_be_valid_url": "A forrás egy érvényes URL kell legyen",
"blocked_users": "Letiltott felhasználók",
"user_unblocked": "Felhasználó letiltva",
"enable_achievement_notifications": "Amikor egy achievement feloldódik",
"launch_minimized": "Hydra indítása minimalizálva",
"disable_nsfw_alert": "NSFW figyelmeztetés kikapcsolása",
"seed_after_download_complete": "Letöltés utáni seedelés",
"show_hidden_achievement_description": "Rejtett achievementek leírásának megjelenítése feloldás előtt",
"account": "Fiók",
"no_users_blocked": "Nincsenek letiltott felhasználóid",
"subscription_active_until": "Hydra Cloud előfizetésed aktív, eddig: {{date}}",
"manage_subscription": "Előfizetés kezelése",
"update_email": "Email változtatása",
"update_password": "Jelszó változtatása",
"current_email": "Jelenlegi email:",
"no_email_account": "Még nincs beállított emailed",
"account_data_updated_successfully": "Fiókadatok változtatása sikeres",
"renew_subscription": "Hydra Cloud Megújítása",
"subscription_expired_at": "Az előfizetésed lejárt, ekkor: {{date}}",
"no_subscription": "Élvezd a Hydrát a lehető legjobb módon",
"become_subscriber": "Légy Hydra Cloud tag",
"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.",
"editor_tab_code": "Code",
"editor_tab_info": "Info",
"editor_tab_save": "Mentés",
"web_store": "Webáruház",
"clear_themes": "Törlés",
"create_theme": "Létrehozás",
"create_theme_modal_title": "Egyéni téma létrehozása",
"create_theme_modal_description": "Hozz létre egy új témát, hogy testreszabhasd a Hydrát ahogy szeretnéd",
"theme_name": "Téma neve",
"insert_theme_name": "Adj a témádnak nevet",
"set_theme": "Téma beállítása",
"unset_theme": "Téma visszavonása",
"delete_theme": "Téma törlése",
"edit_theme": "Téma szerkesztése",
"delete_all_themes": "Összes téma törlése",
"delete_all_themes_description": "Ez törölni fogja az összes témádat",
"delete_theme_description": "Ez törölni fogja a(z) {{theme}} témát",
"cancel": "Mégsem",
"appearance": "Megjelenés",
"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",
"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}}",
"error_importing_theme": "Hiba lépett fel a téma importálása közben",
"theme_imported": "Téma sikeresen importálva",
"enable_friend_request_notifications": "Amikor ismerősnek jelölnek",
"enable_auto_install": "Frissítések letöltése automatikusan",
"common_redist": "Alapvető Segédprogramok",
"common_redist_description": "Egyes játékok futtatásához alapvető segédprogram fájlok szükségesek. A problémák elkerülése képpen ajánlott telepíteni őket.",
"install_common_redist": "Telepítés",
"installing_common_redist": "Telepítés alatt…",
"show_download_speed_in_megabytes": "Letöltési sebesség megabájt/másodpercben lévő megjelenítése",
"extract_files_by_default": "Fájlok kicsomagolása letöltés után",
"enable_steam_achievements": "Steam-achievementek utáni keresés engedélyezése",
"achievement_custom_notification_position": "Achievement-értesítések egyéni elhelyezése",
"top-left": "Bal felső sarok",
"top-center": "Felső közép",
"top-right": "Jobb felső sarok",
"bottom-left": "Bal alsó sarok",
"bottom-center": "Alsó közép",
"bottom-right": "Jobb alsó sarok",
"enable_achievement_custom_notifications": "Egyéni achievement-értesítések bekapcsolása",
"alignment": "Igazítás",
"variation": "Variáció",
"default": "Alapértelmezett",
"rare": "Ritka",
"platinum": "Platinum",
"hidden": "Rejtett",
"test_notification": "Értesítés tesztelése",
"notification_preview": "Achievement Értesítés Előnézete",
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot"
},
"notifications": {
"download_complete": "Letöltés befejeződött",
"game_ready_to_install": "{{title}} telepítésre kész",
"download_complete": "Letöltés befejezve",
"game_ready_to_install": "A(z) {{title}} telepítésre kész",
"repack_list_updated": "Repack lista frissítve",
"repack_count_one": "{{count}} repack hozzáadva",
"repack_count_other": "{{count}} repack hozzáadva"
"repack_count_other": "{{count}} repack hozzáadva",
"new_update_available": "A(z) {{version}} verzió elérhető",
"restart_to_install_update": "Indítsd újra a Hydrát a frissítés telepítéséhez",
"notification_achievement_unlocked_title": "Achievement feloldva: {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} és további {{count}} feloldva",
"new_friend_request_description": "{{displayName}} küldött neked egy barátfelkérést",
"new_friend_request_title": "Új barátfelkérés",
"extraction_complete": "Kicsomagolás befejezve",
"game_extracted": "{{title}} sikeresen kicsomagolva",
"friend_started_playing_game": "{{displayName}} játszani kezdett",
"test_achievement_notification_title": "Ez egy teszt értesítés",
"test_achievement_notification_description": "Elég menő, mi?"
},
"system_tray": {
"open": "Hydra megnyitása",
"quit": "Kilépés"
},
"game_card": {
"available_one": "Elérhető",
"available_other": "Elérhető",
"no_downloads": "Nincs elérhető letöltés"
},
"binary_not_found_modal": {
"title": "A programok nincsenek telepítve",
"description": "A Wine vagy a Lutris végrehajtható fájljai nem találhatók a rendszereden",
"instructions": "Ellenőrizd a megfelelő telepítési módot bármelyiküknek a Linux disztribúciódon, hogy a játék normálisan fusson"
"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"
},
"modal": {
"close": "Bezárás gomb"
},
"forms": {
"toggle_password_visibility": "Jelszó láthatóságának állítása"
},
"user_profile": {
"amount_hours": "{{amount}} óra",
"amount_minutes": "{{amount}} perc",
"amount_hours_short": "{{amount}}ó",
"amount_minutes_short": "{{amount}}p",
"last_time_played": "Utoljára játszva {{period}}",
"activity": "Legutóbbi tevékenység",
"library": "Könyvtár",
"pinned": "Kitűzve",
"achievements_earned": "Elért achievementek",
"played_recently": "Nemrég játszva",
"playtime": "Játszottidő",
"total_play_time": "Teljes játszottidő",
"manual_playtime_tooltip": "Ez a játszottidő manuálisan lett frissítve",
"no_recent_activity_title": "Hmmm… itt semmi sincs",
"no_recent_activity_description": "Mostanában nem játszottál semmivel. Hát ideje ezt megváltoztatni!",
"display_name": "Profilnév",
"saving": "Mentésben",
"save": "Mentés",
"edit_profile": "Profil Szerkesztése",
"saved_successfully": "Sikeresen elmentve",
"try_again": "Kérlek, próbálkozz újra",
"sign_out_modal_title": "Biztos vagy ebben?",
"cancel": "Mégsem",
"successfully_signed_out": "Sikeresen kijelentkezve",
"sign_out": "Kijelentkezés",
"playing_for": "Játékban: {{amount}}",
"sign_out_modal_text": "A könyvtár a jelenlegi fiókodhoz van csatolva. Kijelentkezéskor a könyvtár többé nem lesz látható, és az eddigi előrehaladás nem lesz mentve. Folytatod a kijelentkezést?",
"add_friends": "Barát bejelölés",
"add": "Elküld",
"friend_code": "Barát kód",
"see_profile": "Profil megtekintése",
"sending": "Küldés..",
"friend_request_sent": "Barátfelkérés elküldve",
"friends": "Barátok",
"friends_list": "Barát lista",
"user_not_found": "Felhasználó nem találva",
"block_user": "Felhasználó letiltása",
"add_friend": "Barát bejelölése",
"request_sent": "Kérés elküldve",
"request_received": "Barátfelkérést kaptál",
"accept_request": "Kérés elfogadása",
"ignore_request": "Kérés ignorálása",
"cancel_request": "Kérés visszavonása",
"undo_friendship": "Barát eltávolítása",
"request_accepted": "Barátfelkérés elfogadva",
"user_blocked_successfully": "Felhasználó sikeresen letiltva",
"user_block_modal_text": "Ez által letiltod őt: {{displayName}}",
"blocked_users": "Letiltott felhasználók",
"unblock": "Tiltás feloldása",
"no_friends_added": "Nincs bejelölt barátod",
"pending": "Függőben",
"no_pending_invites": "Nincs függőben lévő barátfelkérésed",
"no_blocked_users": "Nincs letiltott felhasználó",
"friend_code_copied": "Barát kód kimásolva",
"undo_friendship_modal_text": "Ezáltal megszünteted a barátságod vele: {{displayName}}",
"privacy_hint": "Hogy beállítsd ki láthassa ezt, menj a <0>Beállítások</0> menüjébe",
"locked_profile": "Ez a profil privát",
"image_process_failure": "Hiba a kép feldolgozása közben",
"required_field": "Ez a mező kötelező",
"displayname_min_length": "A megjelenített névnek legalább 3 karakter hosszúnak kell lennie",
"displayname_max_length": "A megjelenített név hossza legfeljebb 50 karakter lehet",
"report_profile": "Profil bejelentése",
"report_reason": "Miért jelented ezt a profilt?",
"report_description": "További információ",
"report_description_placeholder": "További információ",
"report": "Bejelentés",
"report_reason_hate": "Gyűlöletbeszéd",
"report_reason_sexual_content": "Szexuális tartalom",
"report_reason_violence": "Fenyegető",
"report_reason_spam": "Spam",
"report_reason_other": "Egyéb",
"profile_reported": "Profil jelentve",
"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…",
"background_image_updated": "Borítókép frissítve",
"stats": "Statisztikák",
"achievements": "achievementek",
"games": "Játékok",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "A rangsor hetente frissül.",
"playing": "Játékban: {{game}}",
"achievements_unlocked": "Achievementek feloldva",
"earned_points": "Megszerzett pontok",
"show_achievements_on_profile": "Mutasd az achievementjeid a profilodon",
"show_points_on_profile": "Mutasd a megszerzett pontjaid a profilodon",
"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"
},
"achievement": {
"achievement_unlocked": "Achievement feloldva",
"user_achievements": "{{displayName}} Achievementjei",
"your_achievements": "A te Achievementjeid",
"unlocked_at": "Feloldva ekkor: {{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",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementek",
"achievements_unlocked_for_game": "{{achievementCount}} új achievementet oldottál fel a(z) {{gameTitle}} játékban",
"hidden_achievement_tooltip": "Ez egy rejtett achievement",
"achievement_earn_points": "Szerezz be {{points}} pontot ezzel az achievement-el",
"earned_points": "Megszerzett pontok:",
"available_points": "Elérhető pontok:",
"how_to_earn_achievements_points": "Hogy lehet elérni achievement pontokat?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Előfizetés",
"subscribe_now": "Előfizetés",
"cloud_saving": "Felhőmentés",
"cloud_achievements": "Mentsd az achievementjeid el a felhőben",
"animated_profile_picture": "Animált profilkép",
"premium_support": "Premium Támogatás",
"show_and_compare_achievements": "Jelenítsd és hasonlítsd az elért achievementjeid másokéhoz",
"animated_profile_banner": "Animált profil borítókép",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Épp felfedeztél egy Hydra Cloud funkciót!",
"learn_more": "Tudj meg többet",
"debrid_description": "Akár 4x gyorsabb letöltés a Nimbusszal"
}
}

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Berhasil masuk"
},
"home": {
"featured": "Unggulan",
"surprise_me": "Kejutkan saya",
"no_results": "Tidak ada hasil ditemukan"
},
@@ -25,7 +24,6 @@
},
"header": {
"search": "Cari game",
"home": "Beranda",
"catalogue": "Katalog",
"downloads": "Unduhan",
@@ -41,10 +39,7 @@
"calculating_eta": "Mengunduh {{title}}… ({{percentage}} selesai) - Menghitung waktu yang tersisa…",
"checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)"
},
"catalogue": {
"next_page": "Halaman Berikutnya",
"previous_page": "Halaman Sebelumnya"
},
"catalogue": {},
"game_details": {
"open_download_options": "Buka opsi unduhan",
"download_options_zero": "Tidak ada opsi unduhan",

View File

@@ -1,7 +1,6 @@
{
"language_name": "Italiano",
"home": {
"featured": "In primo piano",
"surprise_me": "Sorprendimi",
"no_results": "Nessun risultato trovato"
},
@@ -20,7 +19,6 @@
},
"header": {
"search": "Cerca",
"home": "Home",
"catalogue": "Catalogo",
"downloads": "Download",
@@ -32,10 +30,7 @@
"downloading_metadata": "Scaricamento metadati di {{title}}…",
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Pagina successiva",
"previous_page": "Pagina precedente"
},
"catalogue": {},
"game_details": {
"open_download_options": "Apri opzioni di download",
"download_options_zero": "Nessuna opzione di download",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Сәтті кіру"
},
"home": {
"featured": "Ұсынылған",
"surprise_me": "Таңқалдыр",
"no_results": "Ештеңе табылмады"
},
@@ -23,7 +22,6 @@
"sign_in": "Кіру",
"favorites": "Таңдаулылар"
},
"header": {
"search": "Іздеу",
"home": "Басты бет",
@@ -40,10 +38,7 @@
"downloading": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Аяқтау {{eta}} - {{speed}}",
"calculating_eta": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Қалған уақытты есептеу…"
},
"catalogue": {
"next_page": "Келесі бет",
"previous_page": "Алдыңғы бет"
},
"catalogue": {},
"game_details": {
"open_download_options": "Жүктеу нұсқаларын ашу",
"download_options_zero": "Жүктеу нұсқалары жоқ",

View File

@@ -1,7 +1,6 @@
{
"language_name": "한국어",
"home": {
"featured": "추천",
"surprise_me": "무작위 추천",
"no_results": "결과 없음"
},
@@ -17,7 +16,6 @@
"home": "홈",
"favorites": "즐겨찾기"
},
"header": {
"search": "게임 검색하기",
"home": "홈",
@@ -31,10 +29,7 @@
"downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…",
"downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "다음 페이지",
"previous_page": "이전 페이지"
},
"catalogue": {},
"game_details": {
"open_download_options": "다운로드 선택지 열기",
"download_options_zero": "다운로드 선택지 없음",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Logget inn vellykket"
},
"home": {
"featured": "Anbefalinger",
"surprise_me": "Overrask meg",
"no_results": "Ingen resultater fundet",
"start_typing": "Begynn å skrive for å søke...",
@@ -29,7 +28,6 @@
},
"header": {
"search": "Søk efter spill",
"home": "Hjem",
"catalogue": "Katalog",
"downloads": "Nedlastinger",
@@ -45,10 +43,7 @@
"calculating_eta": "Laster ned {{title}}… ({{percentage}} ferdig) - Regner ut resterende tid…",
"checking_files": "Sjekker {{title}} filer… ({{percentage}} ferdig)"
},
"catalogue": {
"next_page": "Neste side",
"previous_page": "Forrige side"
},
"catalogue": {},
"game_details": {
"open_download_options": "Åpne nedlastingsmuligheter",
"download_options_zero": "Ingen nedlastingsmulighet",

View File

@@ -1,7 +1,6 @@
{
"language_name": "Nederlands",
"home": {
"featured": "Uitgelicht",
"surprise_me": "Verrasing",
"no_results": "Geen resultaten gevonden"
},
@@ -19,7 +18,6 @@
},
"header": {
"search": "Zoek spellen",
"home": "Home",
"catalogue": "Bibliotheek",
"downloads": "Downloads",
@@ -31,10 +29,7 @@
"downloading_metadata": "Downloading {{title}} metadata…",
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Volgende Pagina",
"previous_page": "Vorige Pagina"
},
"catalogue": {},
"game_details": {
"open_download_options": "Open download Instellingen",
"download_options_zero": "Geen download Instellingen",

View File

@@ -1,7 +1,6 @@
{
"language_name": "Polski",
"home": {
"featured": "Wyróżnione",
"surprise_me": "Zaskocz mnie",
"no_results": "Nie znaleziono wyników"
},
@@ -20,7 +19,6 @@
},
"header": {
"search": "Szukaj",
"home": "Główna",
"catalogue": "Katalog",
"downloads": "Pobrane",
@@ -32,10 +30,7 @@
"downloading_metadata": "Pobieranie {{title}} metadata…",
"downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Następna strona",
"previous_page": "Poprzednia strona"
},
"catalogue": {},
"game_details": {
"open_download_options": "Otwórz opcje pobierania",
"download_options_zero": "Brak opcji pobierania",

View File

@@ -26,7 +26,22 @@
"sign_in": "Login",
"friends": "Amigos",
"need_help": "Precisa de ajuda?",
"favorites": "Favoritos"
"favorites": "Favoritos",
"add_custom_game_tooltip": "Adicionar jogo personalizado",
"custom_game_modal": "Adicionar jogo personalizado",
"edit_game_modal_title": "Título",
"playable_button_title": "",
"custom_game_modal_add": "Adicionar Jogo",
"custom_game_modal_adding": "Adicionando...",
"custom_game_modal_browse": "Buscar",
"custom_game_modal_cancel": "Cancelar",
"edit_game_modal_assets": "Imagens",
"edit_game_modal_icon": "Ícone",
"edit_game_modal_browse": "Buscar",
"edit_game_modal_cancel": "Cancelar",
"edit_game_modal_enter_title": "Insira o título",
"edit_game_modal_logo": "Logo",
"edit_game_modal": "Personalizar detalhes"
},
"header": {
"search": "Buscar jogos",
@@ -228,7 +243,20 @@
"historical_keyshop": "Preço histórico em keyshops",
"language": "Idioma",
"caption": "Legenda",
"audio": "Áudio"
"audio": "Áudio",
"filter_by_source": "Filtrar por fonte",
"no_repacks_found": "Nenhuma fonte encontrada para este jogo",
"edit_game_modal_button": "Alterar detalhes do jogo",
"game_added_to_pinned": "Jogo adicionado aos fixados",
"game_removed_from_pinned": "Jogo removido dos fixados",
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
"manual_playtime_warning": "As suas horas de jogo serão marcadas como atualizadas manualmente. Esta ação não pode ser desfeita.",
"missing_wine_prefix": "Um prefixo Wine é necessário para criar um backup no Linux",
"update_game_playtime": "Modificar tempo de jogo",
"update_playtime": "Modificar tempo de jogo",
"update_playtime_description": "Atualizar manualmente o tempo de jogo de {{game}}",
"update_playtime_error": "Falha ao atualizar tempo de jogo",
"update_playtime_title": "Atualizar tempo de jogo"
},
"activation": {
"title": "Ativação",
@@ -403,7 +431,8 @@
"hidden": "Oculta",
"test_notification": "Testar notificação",
"notification_preview": "Prévia da Notificação de Conquistas",
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
"editor_tab_code": "Código"
},
"notifications": {
"download_complete": "Download concluído",
@@ -532,7 +561,15 @@
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
"friend_code_length_error": "Código de amigo deve ter 8 caracteres"
"friend_code_length_error": "Código de amigo deve ter 8 caracteres",
"top_percentile": "Top {{percentile}}%",
"playtime": "Tempo de jogo",
"played_recently": "Jogado recentemente",
"pinned": "Fixado",
"amount_minutes_short": "{{amount}}m",
"amount_hours_short": "{{amount}}h",
"game_added_to_pinned": "Jogo adicionado aos fixados",
"achievements_earned": "Conquistas recebidas"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Sessão iniciada com sucesso"
},
"home": {
"featured": "Destaques",
"hot": "Populares",
"weekly": "📅 Mais descarregados esta semana",
"achievements": "🏆 Para completar",
@@ -26,7 +25,8 @@
"game_has_no_executable": "O jogo não tem um executável selecionado",
"sign_in": "Iniciar sessão",
"friends": "Amigos",
"favorites": "Favoritos"
"favorites": "Favoritos",
"edit_game_modal_cancel": "Cancelar"
},
"header": {
"search": "Procurar jogos",
@@ -247,9 +247,6 @@
"download_count_zero": "Sem downloads na lista",
"download_count_one": "{{countFormatted}} download na lista",
"download_count_other": "{{countFormatted}} downloads na lista",
"download_options_zero": "Sem downloads disponíveis",
"download_options_one": "{{countFormatted}} download disponível",
"download_options_other": "{{countFormatted}} downloads disponíveis",
"download_source_url": "URL da fonte",
"add_download_source_description": "Insere o URL que contém o ficheiro .json",
"download_source_up_to_date": "Sincronizada",
@@ -359,8 +356,6 @@
"instructions": "Verifica a forma correta de instalar algum deles na tua distribuição Linux, para garantir a execução normal do jogo"
},
"catalogue": {
"next_page": "Página seguinte",
"previous_page": "Página anterior",
"search": "Filtrar…",
"developers": "Desenvolvedores",
"genres": "Géneros",
@@ -427,7 +422,6 @@
"friend_code_copied": "Código de amigo copiado",
"undo_friendship_modal_text": "Isto vai remover a tua amizade com {{displayName}}",
"privacy_hint": "Para controlar quem pode ver o teu perfil, acede às <0>Definições</0>",
"profile_locked": "Este perfil é privado",
"image_process_failure": "Falha ao processar a imagem",
"required_field": "Este campo é obrigatório",
"displayname_min_length": "O nome de apresentação deve ter pelo menos 3 caracteres",

View File

@@ -1,7 +1,6 @@
{
"language_name": "Română",
"home": {
"featured": "Recomandate",
"surprise_me": "Surprinde-mă",
"no_results": "Niciun rezultat găsit"
},
@@ -19,7 +18,6 @@
},
"header": {
"search": "Caută jocuri",
"home": "Acasă",
"catalogue": "Catalog",
"downloads": "Descărcări",
@@ -32,10 +30,7 @@
"downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}",
"calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..."
},
"catalogue": {
"next_page": "Pagina următoare",
"previous_page": "Pagina anterioară"
},
"catalogue": {},
"game_details": {
"open_download_options": "Deschide opțiunile de descărcare",
"download_options_zero": "Nicio opțiune de descărcare",

View File

@@ -4,14 +4,12 @@
"successfully_signed_in": "Успешный вход"
},
"home": {
"featured": "Рекомендации",
"surprise_me": "Удиви меня",
"no_results": "Ничего не найдено",
"hot": "Сейчас популярно",
"start_typing": "Начинаю вводить текст...",
"weekly": "📅 Лучшие игры недели",
"achievements": "🏆 Игры с достижениями",
"already_in_library": "Уже в библиотеке"
"achievements": "🏆 Игры с достижениями"
},
"sidebar": {
"catalogue": "Каталог",
@@ -69,7 +67,14 @@
"edit_game_modal_image_filter": "Изображение",
"edit_game_modal_icon_resolution": "Рекомендуемое разрешение: 256x256px",
"edit_game_modal_logo_resolution": "Рекомендуемое разрешение: 640x360px",
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px"
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px",
"edit_game_modal_assets": "Ресурсы",
"edit_game_modal_drop_icon_image_here": "Перетащите изображение иконки сюда",
"edit_game_modal_drop_logo_image_here": "Перетащите изображение логотипа сюда",
"edit_game_modal_drop_hero_image_here": "Перетащите изображение обложки сюда",
"edit_game_modal_drop_to_replace_icon": "Перетащите для замены иконки",
"edit_game_modal_drop_to_replace_logo": "Перетащите для замены логотипа",
"edit_game_modal_drop_to_replace_hero": "Перетащите для замены обложки"
},
"header": {
"search": "Поиск",
@@ -486,6 +491,8 @@
"user_profile": {
"amount_hours": "{{amount}} часов",
"amount_minutes": "{{amount}} минут",
"amount_hours_short": "{{amount}}ч",
"amount_minutes_short": "{{amount}}м",
"last_time_played": "Последняя игра {{period}}",
"activity": "Недавняя активность",
"library": "Библиотека",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Inloggningen lyckades"
},
"home": {
"featured": "Utvalt",
"surprise_me": "Överraska mig",
"no_results": "Inga resultat hittades",
"start_typing": "Börja skriva för att söka...",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Başarıyla giriş yapıldı"
},
"home": {
"featured": "Öne Çıkanlar",
"surprise_me": "Beni Şaşırt",
"no_results": "Sonuç bulunamadı",
"start_typing": "Aramak için yazmaya başlayın...",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Успішний вхід в систему"
},
"home": {
"featured": "Рекомендоване",
"surprise_me": "Здивуй мене",
"no_results": "Результатів не знайдено",
"start_typing": "Почніть набирати текст для пошуку...",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "Kirish muvaffaqiyatli amalga oshirildi"
},
"home": {
"featured": "Tavsiya etilgan",
"surprise_me": "Hayratda qoldir",
"no_results": "Natijalar topilmadi",
"hot": "Eng mashhur",

View File

@@ -4,7 +4,6 @@
"successfully_signed_in": "已成功登录"
},
"home": {
"featured": "特色推荐",
"surprise_me": "向我推荐",
"no_results": "没有找到结果",
"start_typing": "键入以开始搜素...",
@@ -51,8 +50,6 @@
"installing_common_redist": "{{log}}…"
},
"catalogue": {
"next_page": "下一页",
"previous_page": "上一页",
"clear_filters": "清除已选的 {{filterCount}} 项",
"developers": "开发商",
"download_sources": "下载源",

View File

@@ -21,11 +21,9 @@ const getGameStats = async (
return cachedStats;
}
return HydraApi.get<GameStats>(
`/games/stats`,
{ objectId, shop },
{ needsAuth: false }
).then(async (data) => {
return HydraApi.get<GameStats>(`/games/${shop}/${objectId}/stats`, null, {
needsAuth: false,
}).then(async (data) => {
await gamesStatsCacheSublevel.put(levelKeys.game(shop, objectId), {
...data,
updatedAt: Date.now(),

View File

@@ -8,12 +8,7 @@ const getHowLongToBeat = async (
objectId: string,
shop: GameShop
): Promise<HowLongToBeatCategory[] | null> => {
const params = new URLSearchParams({
objectId,
shop,
});
return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, {
return HydraApi.get(`/games/${shop}/${objectId}/how-long-to-beat`, null, {
needsAuth: false,
});
};

View File

@@ -11,7 +11,7 @@ const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
.then((language) => language || "en");
const trendingGames = await HydraApi.get<TrendingGame[]>(
"/games/featured",
"/catalogue/featured",
{ language },
{ needsAuth: false }
).catch(() => []);

View File

@@ -87,7 +87,7 @@ const createSteamShortcut = async (
}
const { assets } = await HydraApi.get<GameStats>(
`/games/stats?objectId=${objectId}&shop=${shop}`
`/games/${shop}/${objectId}/stats`
);
const steamUserIds = await getSteamUsersIds();

View File

@@ -1,7 +1,70 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { gamesSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
import { HydraApi, logger } from "@main/services";
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
import type { GameShop, Game } from "@types";
import fs from "node:fs";
const collectAssetPathsToDelete = (game: Game): string[] => {
const assetPathsToDelete: string[] = [];
const assetUrls =
game.shop === "custom"
? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl]
: [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl];
for (const url of assetUrls) {
if (url?.startsWith("local:")) {
assetPathsToDelete.push(url.replace("local:", ""));
}
}
return assetPathsToDelete;
};
const updateGameAsDeleted = async (
game: Game,
gameKey: string
): Promise<void> => {
const updatedGame = {
...game,
isDeleted: true,
executablePath: null,
...(game.shop !== "custom" && {
customIconUrl: null,
customLogoImageUrl: null,
customHeroImageUrl: null,
}),
};
await gamesSublevel.put(gameKey, updatedGame);
};
const resetShopAssets = async (gameKey: string): Promise<void> => {
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
if (existingAssets) {
const resetAssets = {
...existingAssets,
title: existingAssets.title,
};
await gamesShopAssetsSublevel.put(gameKey, resetAssets);
}
};
const deleteAssetFiles = async (
assetPathsToDelete: string[]
): Promise<void> => {
if (assetPathsToDelete.length === 0) return;
for (const assetPath of assetPathsToDelete) {
try {
if (fs.existsSync(assetPath)) {
await fs.promises.unlink(assetPath);
}
} catch (error) {
logger.warn(`Failed to delete asset ${assetPath}:`, error);
}
}
};
const removeGameFromLibrary = async (
_event: Electron.IpcMainInvokeEvent,
@@ -11,17 +74,21 @@ const removeGameFromLibrary = async (
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (game) {
await gamesSublevel.put(gameKey, {
...game,
isDeleted: true,
executablePath: null,
});
if (!game) return;
if (game?.remoteId) {
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
}
const assetPathsToDelete = collectAssetPathsToDelete(game);
await updateGameAsDeleted(game, gameKey);
if (game.shop !== "custom") {
await resetShopAssets(gameKey);
}
if (game?.remoteId) {
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
}
await deleteAssetFiles(assetPathsToDelete);
};
registerEvent("removeGameFromLibrary", removeGameFromLibrary);

View File

@@ -1,16 +1,36 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
import fs from "node:fs";
import { logger } from "@main/services";
interface UpdateCustomGameParams {
shop: GameShop;
objectId: string;
title: string;
iconUrl?: string;
logoImageUrl?: string;
libraryHeroImageUrl?: string;
originalIconPath?: string;
originalLogoPath?: string;
originalHeroPath?: string;
}
const updateCustomGame = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string,
title: string,
iconUrl?: string,
logoImageUrl?: string,
libraryHeroImageUrl?: string
params: UpdateCustomGameParams
) => {
const {
shop,
objectId,
title,
iconUrl,
logoImageUrl,
libraryHeroImageUrl,
originalIconPath,
originalLogoPath,
originalHeroPath,
} = params;
const gameKey = levelKeys.game(shop, objectId);
const existingGame = await gamesSublevel.get(gameKey);
@@ -18,12 +38,29 @@ const updateCustomGame = async (
throw new Error("Game not found");
}
const oldAssetPaths: string[] = [];
const assetPairs = [
{ existing: existingGame.iconUrl, new: iconUrl },
{ existing: existingGame.logoImageUrl, new: logoImageUrl },
{ existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl },
];
for (const { existing, new: newUrl } of assetPairs) {
if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) {
oldAssetPaths.push(existing.replace("local:", ""));
}
}
const updatedGame = {
...existingGame,
title,
iconUrl: iconUrl || null,
logoImageUrl: logoImageUrl || null,
libraryHeroImageUrl: libraryHeroImageUrl || null,
originalIconPath: originalIconPath || existingGame.originalIconPath || null,
originalLogoPath: originalLogoPath || existingGame.originalLogoPath || null,
originalHeroPath: originalHeroPath || existingGame.originalHeroPath || null,
};
await gamesSublevel.put(gameKey, updatedGame);
@@ -43,6 +80,18 @@ const updateCustomGame = async (
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
}
if (oldAssetPaths.length > 0) {
for (const assetPath of oldAssetPaths) {
try {
if (fs.existsSync(assetPath)) {
await fs.promises.unlink(assetPath);
}
} catch (error) {
logger.warn(`Failed to delete old asset ${assetPath}:`, error);
}
}
}
return updatedGame;
};

View File

@@ -1,16 +1,131 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
import type { GameShop, Game } from "@types";
import fs from "node:fs";
import { logger } from "@main/services";
const updateGameCustomAssets = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string,
title: string,
const collectOldAssetPaths = (
existingGame: Game,
customIconUrl?: string | null,
customLogoImageUrl?: string | null,
customHeroImageUrl?: string | null
): string[] => {
const oldAssetPaths: string[] = [];
const assetPairs = [
{ existing: existingGame.customIconUrl, new: customIconUrl },
{ existing: existingGame.customLogoImageUrl, new: customLogoImageUrl },
{ existing: existingGame.customHeroImageUrl, new: customHeroImageUrl },
];
for (const { existing, new: newUrl } of assetPairs) {
if (
existing &&
newUrl !== undefined &&
existing !== newUrl &&
existing.startsWith("local:")
) {
oldAssetPaths.push(existing.replace("local:", ""));
}
}
return oldAssetPaths;
};
interface UpdateGameDataParams {
gameKey: string;
existingGame: Game;
title: string;
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
}
const updateGameData = async (params: UpdateGameDataParams): Promise<Game> => {
const {
gameKey,
existingGame,
title,
customIconUrl,
customLogoImageUrl,
customHeroImageUrl,
customOriginalIconPath,
customOriginalLogoPath,
customOriginalHeroPath,
} = params;
const updatedGame = {
...existingGame,
title,
...(customIconUrl !== undefined && { customIconUrl }),
...(customLogoImageUrl !== undefined && { customLogoImageUrl }),
...(customHeroImageUrl !== undefined && { customHeroImageUrl }),
...(customOriginalIconPath !== undefined && { customOriginalIconPath }),
...(customOriginalLogoPath !== undefined && { customOriginalLogoPath }),
...(customOriginalHeroPath !== undefined && { customOriginalHeroPath }),
};
await gamesSublevel.put(gameKey, updatedGame);
return updatedGame;
};
const updateShopAssets = async (
gameKey: string,
title: string
): Promise<void> => {
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
if (existingAssets) {
const updatedAssets = {
...existingAssets,
title,
};
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
}
};
const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise<void> => {
if (oldAssetPaths.length === 0) return;
for (const assetPath of oldAssetPaths) {
try {
if (fs.existsSync(assetPath)) {
await fs.promises.unlink(assetPath);
}
} catch (error) {
logger.warn(`Failed to delete old custom asset ${assetPath}:`, error);
}
}
};
interface UpdateGameCustomAssetsParams {
shop: GameShop;
objectId: string;
title: string;
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
}
const updateGameCustomAssets = async (
_event: Electron.IpcMainInvokeEvent,
params: UpdateGameCustomAssetsParams
) => {
const {
shop,
objectId,
title,
customIconUrl,
customLogoImageUrl,
customHeroImageUrl,
customOriginalIconPath,
customOriginalLogoPath,
customOriginalHeroPath,
} = params;
const gameKey = levelKeys.game(shop, objectId);
const existingGame = await gamesSublevel.get(gameKey);
@@ -18,26 +133,28 @@ const updateGameCustomAssets = async (
throw new Error("Game not found");
}
const updatedGame = {
...existingGame,
const oldAssetPaths = collectOldAssetPaths(
existingGame,
customIconUrl,
customLogoImageUrl,
customHeroImageUrl
);
const updatedGame = await updateGameData({
gameKey,
existingGame,
title,
...(customIconUrl !== undefined && { customIconUrl }),
...(customLogoImageUrl !== undefined && { customLogoImageUrl }),
...(customHeroImageUrl !== undefined && { customHeroImageUrl }),
};
customIconUrl,
customLogoImageUrl,
customHeroImageUrl,
customOriginalIconPath,
customOriginalLogoPath,
customOriginalHeroPath,
});
await gamesSublevel.put(gameKey, updatedGame);
await updateShopAssets(gameKey, title);
// Also update the shop assets for non-custom games
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
if (existingAssets) {
const updatedAssets = {
...existingAssets,
title, // Update the title in shop assets as well
};
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
}
await deleteOldAssetFiles(oldAssetPaths);
return updatedGame;
};

View File

@@ -93,14 +93,9 @@ const startGameDownload = async (
await Promise.all([
createGame(updatedGame!).catch(() => {}),
HydraApi.post(
"/games/download",
{
objectId,
shop,
},
{ needsAuth: false }
).catch(() => {}),
HydraApi.post(`/games/${shop}/${objectId}/download`, null, {
needsAuth: false,
}).catch(() => {}),
]);
return { ok: true };

View File

@@ -45,10 +45,8 @@ export const getGameAchievementData = async (
.then((language) => language || "en");
return HydraApi.get<SteamAchievement[]>(
"/games/achievements",
`/games/${shop}/${objectId}/achievements`,
{
shop,
objectId,
language,
},
{

View File

@@ -10,7 +10,7 @@ export const requestSteam250 = async (path: string) => {
const { window } = new JSDOM(response.data);
const { document } = window;
return Array.from(document.querySelectorAll(".appline .title a"))
return Array.from(document.querySelectorAll("a[data-title]"))
.map(($title) => {
const steamGameUrl = ($title as HTMLAnchorElement).href;
if (!steamGameUrl) return null;

View File

@@ -16,9 +16,7 @@ export const friendGameSessionEvent = async (payload: FriendGameSession) => {
const [friend, gameStats] = await Promise.all([
HydraApi.get<UserProfile>(`/users/${payload.friendId}`),
HydraApi.get<GameStats>(
`/games/stats?objectId=${payload.objectId}&shop=steam`
),
HydraApi.get<GameStats>(`/games/steam/${payload.objectId}/stats`),
]).catch(() => [null, null]);
if (friend && gameStats) {

View File

@@ -152,40 +152,28 @@ contextBridge.exposeInMainWorld("electron", {
deleteTempFile: (filePath: string) =>
ipcRenderer.invoke("deleteTempFile", filePath),
cleanupUnusedAssets: () => ipcRenderer.invoke("cleanupUnusedAssets"),
updateCustomGame: (
shop: GameShop,
objectId: string,
title: string,
iconUrl?: string,
logoImageUrl?: string,
libraryHeroImageUrl?: string
) =>
ipcRenderer.invoke(
"updateCustomGame",
shop,
objectId,
title,
iconUrl,
logoImageUrl,
libraryHeroImageUrl
),
updateGameCustomAssets: (
shop: GameShop,
objectId: string,
title: string,
customIconUrl?: string | null,
customLogoImageUrl?: string | null,
customHeroImageUrl?: string | null
) =>
ipcRenderer.invoke(
"updateGameCustomAssets",
shop,
objectId,
title,
customIconUrl,
customLogoImageUrl,
customHeroImageUrl
),
updateCustomGame: (params: {
shop: GameShop;
objectId: string;
title: string;
iconUrl?: string;
logoImageUrl?: string;
libraryHeroImageUrl?: string;
originalIconPath?: string;
originalLogoPath?: string;
originalHeroPath?: string;
}) => ipcRenderer.invoke("updateCustomGame", params),
updateGameCustomAssets: (params: {
shop: GameShop;
objectId: string;
title: string;
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
}) => ipcRenderer.invoke("updateGameCustomAssets", params),
createGameShortcut: (
shop: GameShop,
objectId: string,

View File

@@ -119,14 +119,17 @@ declare global {
logoImageUrl?: string,
libraryHeroImageUrl?: string
) => Promise<Game>;
updateCustomGame: (
shop: GameShop,
objectId: string,
title: string,
iconUrl?: string,
logoImageUrl?: string,
libraryHeroImageUrl?: string
) => Promise<Game>;
updateCustomGame: (params: {
shop: GameShop;
objectId: string;
title: string;
iconUrl?: string;
logoImageUrl?: string;
libraryHeroImageUrl?: string;
originalIconPath?: string;
originalLogoPath?: string;
originalHeroPath?: string;
}) => Promise<Game>;
copyCustomGameAsset: (
sourcePath: string,
assetType: "icon" | "logo" | "hero"
@@ -135,14 +138,17 @@ declare global {
deletedCount: number;
errors: string[];
}>;
updateGameCustomAssets: (
shop: GameShop,
objectId: string,
title: string,
customIconUrl?: string | null,
customLogoImageUrl?: string | null,
customHeroImageUrl?: string | null
) => Promise<Game>;
updateGameCustomAssets: (params: {
shop: GameShop;
objectId: string;
title: string;
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
}) => Promise<Game>;
createGameShortcut: (
shop: GameShop,
objectId: string,

View File

@@ -1,8 +1,7 @@
@use "../../../scss/globals.scss";
.description-header {
width: calc(100% - calc(globals.$spacing-unit * 2));
margin: calc(globals.$spacing-unit * 1) auto;
width: 100%;
padding: calc(globals.$spacing-unit * 1.5);
display: flex;
justify-content: space-between;
@@ -10,8 +9,9 @@
background-color: globals.$background-color;
height: 72px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.03);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin-bottom: calc(globals.$spacing-unit * 1);
&__info {
display: flex;

View File

@@ -7,6 +7,15 @@
display: flex;
flex-direction: column;
align-items: center;
max-height: 80vh;
@media (min-width: 1024px) {
max-height: 70vh;
}
@media (min-width: 1280px) {
max-height: 60vh;
}
}
&__viewport {
@@ -16,8 +25,19 @@
overflow: hidden;
border-radius: 8px;
@media (min-width: 1024px) {
width: 80%;
max-height: 400px;
}
@media (min-width: 1280px) {
width: 60%;
max-height: 500px;
}
@media (min-width: 1536px) {
width: 50%;
max-height: 600px;
}
}
@@ -52,10 +72,18 @@
overflow-y: hidden;
gap: calc(globals.$spacing-unit / 2);
@media (min-width: 1024px) {
width: 80%;
}
@media (min-width: 1280px) {
width: 60%;
}
@media (min-width: 1536px) {
width: 50%;
}
&::-webkit-scrollbar-thumb {
width: 20%;
}
@@ -79,6 +107,19 @@
border: solid 1px globals.$border-color;
overflow: hidden;
position: relative;
aspect-ratio: 16/9;
@media (min-width: 1024px) {
width: 15%;
}
@media (min-width: 1280px) {
width: 12%;
}
@media (min-width: 1536px) {
width: 10%;
}
&:hover {
opacity: 0.8;

View File

@@ -43,6 +43,29 @@ export function GameDetailsContent() {
const $images = Array.from(document.querySelectorAll("img"));
$images.forEach(($image) => {
$image.loading = "lazy";
// Remove any inline width/height styles that might cause overflow
$image.removeAttribute("width");
$image.removeAttribute("height");
$image.removeAttribute("style");
// Set max-width to prevent overflow
$image.style.maxWidth = "100%";
$image.style.width = "auto";
$image.style.height = "auto";
$image.style.boxSizing = "border-box";
});
// Handle videos the same way
const $videos = Array.from(document.querySelectorAll("video"));
$videos.forEach(($video) => {
// Remove any inline width/height styles that might cause overflow
$video.removeAttribute("width");
$video.removeAttribute("height");
$video.removeAttribute("style");
// Set max-width to prevent overflow
$video.style.maxWidth = "100%";
$video.style.width = "auto";
$video.style.height = "auto";
$video.style.boxSizing = "border-box";
});
return document.body.outerHTML;
@@ -168,14 +191,16 @@ export function GameDetailsContent() {
{renderGameLogo()}
<div className="game-details__hero-buttons game-details__hero-buttons--right">
<button
type="button"
className="game-details__edit-custom-game-button"
onClick={handleEditGameClick}
title={t("edit_game_modal_button")}
>
<PencilIcon size={16} />
</button>
{game && (
<button
type="button"
className="game-details__edit-custom-game-button"
onClick={handleEditGameClick}
title={t("edit_game_modal_button")}
>
<PencilIcon size={16} />
</button>
)}
{game?.shop !== "custom" && (
<button
@@ -217,13 +242,15 @@ export function GameDetailsContent() {
</div>
</section>
<EditGameModal
visible={showEditGameModal}
onClose={() => setShowEditGameModal(false)}
game={game}
shopDetails={shopDetails}
onGameUpdated={handleGameUpdated}
/>
{game && (
<EditGameModal
visible={showEditGameModal}
onClose={() => setShowEditGameModal(false)}
game={game}
shopDetails={shopDetails}
onGameUpdated={handleGameUpdated}
/>
)}
</div>
);
}

View File

@@ -182,44 +182,49 @@ $hero-height: 300px;
globals.$background-color 50%,
globals.$dark-background-color 100%
);
padding: calc(globals.$spacing-unit * 1.5);
gap: calc(globals.$spacing-unit * 1.5);
}
&__description-content {
width: 100%;
height: 100%;
min-height: 100%;
min-width: 0;
flex: 1;
overflow-x: hidden;
}
&__description {
user-select: text;
line-height: 22px;
font-size: globals.$body-font-size;
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5);
width: 100%;
margin-left: auto;
margin-right: auto;
@media (min-width: 768px) {
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
}
@media (min-width: 1024px) {
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
}
overflow-x: auto;
min-height: auto;
@media (min-width: 1280px) {
width: 60%;
}
img {
@media (min-width: 1536px) {
width: 50%;
}
img,
video {
border-radius: 5px;
margin-top: globals.$spacing-unit;
margin-bottom: calc(globals.$spacing-unit * 3);
display: block;
width: 100%;
height: auto;
object-fit: cover;
display: block !important;
max-width: 100% !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
box-sizing: border-box !important;
word-wrap: break-word;
overflow-wrap: break-word;
}
a {
@@ -247,12 +252,17 @@ $hero-height: 300px;
@media (min-width: 1024px) {
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
width: 80%;
}
@media (min-width: 1280px) {
width: 60%;
line-height: 22px;
}
@media (min-width: 1536px) {
width: 50%;
}
}
&__randomizer-button {

View File

@@ -1,9 +1,10 @@
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { ImageIcon, XIcon } from "@primer/octicons-react";
import { Modal, TextField, Button } from "@renderer/components";
import { useToast } from "@renderer/hooks";
import { generateRandomGradient } from "@renderer/helpers";
import type { LibraryGame, Game, ShopDetailsWithAssets } from "@types";
import "./edit-game-modal.scss";
@@ -29,16 +30,34 @@ export function EditGameModal({
const { showSuccessToast, showErrorToast } = useToast();
const [gameName, setGameName] = useState("");
const [iconPath, setIconPath] = useState("");
const [logoPath, setLogoPath] = useState("");
const [heroPath, setHeroPath] = useState("");
const [assetPaths, setAssetPaths] = useState({
icon: "",
logo: "",
hero: "",
});
const [assetDisplayPaths, setAssetDisplayPaths] = useState({
icon: "",
logo: "",
hero: "",
});
const [originalAssetPaths, setOriginalAssetPaths] = useState({
icon: "",
logo: "",
hero: "",
});
const [removedAssets, setRemovedAssets] = useState({
icon: false,
logo: false,
hero: false,
});
const [defaultUrls, setDefaultUrls] = useState({
icon: null as string | null,
logo: null as string | null,
hero: null as string | null,
});
const [isUpdating, setIsUpdating] = useState(false);
const [selectedAssetType, setSelectedAssetType] = useState<AssetType>("icon");
const [defaultIconUrl, setDefaultIconUrl] = useState<string | null>(null);
const [defaultLogoUrl, setDefaultLogoUrl] = useState<string | null>(null);
const [defaultHeroUrl, setDefaultHeroUrl] = useState<string | null>(null);
const isCustomGame = (game: LibraryGame | Game): boolean => {
return game.shop === "custom";
};
@@ -48,26 +67,58 @@ export function EditGameModal({
};
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
setIconPath(extractLocalPath(game.iconUrl));
setLogoPath(extractLocalPath(game.logoImageUrl));
setHeroPath(extractLocalPath(game.libraryHeroImageUrl));
setAssetPaths({
icon: extractLocalPath(game.iconUrl),
logo: extractLocalPath(game.logoImageUrl),
hero: extractLocalPath(game.libraryHeroImageUrl),
});
setAssetDisplayPaths({
icon: extractLocalPath(game.iconUrl),
logo: extractLocalPath(game.logoImageUrl),
hero: extractLocalPath(game.libraryHeroImageUrl),
});
setOriginalAssetPaths({
icon: (game as any).originalIconPath || extractLocalPath(game.iconUrl),
logo:
(game as any).originalLogoPath || extractLocalPath(game.logoImageUrl),
hero:
(game as any).originalHeroPath ||
extractLocalPath(game.libraryHeroImageUrl),
});
}, []);
const setNonCustomGameAssets = useCallback(
(game: LibraryGame) => {
setIconPath(extractLocalPath(game.customIconUrl));
setLogoPath(extractLocalPath(game.customLogoImageUrl));
setHeroPath(extractLocalPath(game.customHeroImageUrl));
setAssetPaths({
icon: extractLocalPath(game.customIconUrl),
logo: extractLocalPath(game.customLogoImageUrl),
hero: extractLocalPath(game.customHeroImageUrl),
});
setAssetDisplayPaths({
icon: extractLocalPath(game.customIconUrl),
logo: extractLocalPath(game.customLogoImageUrl),
hero: extractLocalPath(game.customHeroImageUrl),
});
setOriginalAssetPaths({
icon:
(game as any).customOriginalIconPath ||
extractLocalPath(game.customIconUrl),
logo:
(game as any).customOriginalLogoPath ||
extractLocalPath(game.customLogoImageUrl),
hero:
(game as any).customOriginalHeroPath ||
extractLocalPath(game.customHeroImageUrl),
});
setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null);
setDefaultLogoUrl(
shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null
);
setDefaultHeroUrl(
shopDetails?.assets?.libraryHeroImageUrl ||
setDefaultUrls({
icon: shopDetails?.assets?.iconUrl || game.iconUrl || null,
logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null,
hero:
shopDetails?.assets?.libraryHeroImageUrl ||
game.libraryHeroImageUrl ||
null
);
null,
});
},
[shopDetails]
);
@@ -93,38 +144,38 @@ export function EditGameModal({
};
const getAssetPath = (assetType: AssetType): string => {
switch (assetType) {
case "icon":
return iconPath;
case "logo":
return logoPath;
case "hero":
return heroPath;
}
return assetPaths[assetType];
};
const getAssetDisplayPath = (assetType: AssetType): string => {
// Use original path if available, otherwise fall back to display path
return originalAssetPaths[assetType] || assetDisplayPaths[assetType];
};
const setAssetPath = (assetType: AssetType, path: string): void => {
switch (assetType) {
case "icon":
setIconPath(path);
break;
case "logo":
setLogoPath(path);
break;
case "hero":
setHeroPath(path);
break;
}
setAssetPaths((prev) => ({ ...prev, [assetType]: path }));
};
const setAssetDisplayPath = (assetType: AssetType, path: string): void => {
setAssetDisplayPaths((prev) => ({ ...prev, [assetType]: path }));
};
const getDefaultUrl = (assetType: AssetType): string | null => {
return defaultUrls[assetType];
};
const getOriginalAssetUrl = (assetType: AssetType): string | null => {
if (!game || !isCustomGame(game)) return null;
switch (assetType) {
case "icon":
return defaultIconUrl;
return game.iconUrl;
case "logo":
return defaultLogoUrl;
return game.logoImageUrl;
case "hero":
return defaultHeroUrl;
return game.libraryHeroImageUrl;
default:
return null;
}
};
@@ -140,23 +191,68 @@ export function EditGameModal({
});
if (filePaths && filePaths.length > 0) {
const originalPath = filePaths[0];
try {
const copiedAssetUrl = await window.electron.copyCustomGameAsset(
filePaths[0],
originalPath,
assetType
);
setAssetPath(assetType, copiedAssetUrl.replace("local:", ""));
setAssetDisplayPath(assetType, originalPath);
// Store the original path for display purposes
setOriginalAssetPaths((prev) => ({
...prev,
[assetType]: originalPath,
}));
// Clear the removed flag when a new asset is selected
setRemovedAssets((prev) => ({ ...prev, [assetType]: false }));
} catch (error) {
console.error(`Failed to copy ${assetType} asset:`, error);
setAssetPath(assetType, filePaths[0]);
setAssetPath(assetType, originalPath);
setAssetDisplayPath(assetType, originalPath);
setOriginalAssetPaths((prev) => ({
...prev,
[assetType]: originalPath,
}));
// Clear the removed flag when a new asset is selected
setRemovedAssets((prev) => ({ ...prev, [assetType]: false }));
}
}
};
const handleRestoreDefault = (assetType: AssetType) => {
setAssetPath(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]: "" }));
}
};
const getOriginalTitle = (): string => {
if (!game) return "";
// For non-custom games, the original title is from shopDetails assets
return shopDetails?.assets?.title || game.title || "";
};
const handleRestoreDefaultTitle = () => {
const originalTitle = getOriginalTitle();
setGameName(originalTitle);
};
const isTitleChanged = useMemo((): boolean => {
if (!game || isCustomGame(game)) return false;
const originalTitle = getOriginalTitle();
return gameName.trim() !== originalTitle.trim();
}, [game, gameName, shopDetails]);
const [dragOverTarget, setDragOverTarget] = useState<string | null>(null);
const handleDragOver = (e: React.DragEvent) => {
@@ -232,6 +328,7 @@ export function EditGameModal({
const assetPath = copiedAssetUrl.replace("local:", "");
setAssetPath(assetType, assetPath);
setAssetDisplayPath(assetType, filePath);
showSuccessToast(
`${assetType.charAt(0).toUpperCase() + assetType.slice(1)} updated successfully!`
@@ -267,11 +364,38 @@ export function EditGameModal({
// Helper function to prepare custom game assets
const prepareCustomGameAssets = (game: LibraryGame | Game) => {
const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl;
const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl;
const libraryHeroImageUrl = heroPath
? `local:${heroPath}`
: game.libraryHeroImageUrl;
// For custom games, check if asset was explicitly removed
let iconUrl;
if (removedAssets.icon) {
iconUrl = null;
} else if (assetPaths.icon) {
iconUrl = `local:${assetPaths.icon}`;
} else {
iconUrl = game.iconUrl;
}
let logoImageUrl;
if (removedAssets.logo) {
logoImageUrl = null;
} else if (assetPaths.logo) {
logoImageUrl = `local:${assetPaths.logo}`;
} else {
logoImageUrl = game.logoImageUrl;
}
// For hero image, if removed, restore to the original gradient or keep the original
let libraryHeroImageUrl;
if (removedAssets.hero) {
// If the original hero was a gradient (data URL), keep it, otherwise generate a new one
const originalHero = game.libraryHeroImageUrl;
libraryHeroImageUrl = originalHero?.startsWith("data:image/svg+xml")
? originalHero
: generateRandomGradient();
} else {
libraryHeroImageUrl = assetPaths.hero
? `local:${assetPaths.hero}`
: game.libraryHeroImageUrl;
}
return { iconUrl, logoImageUrl, libraryHeroImageUrl };
};
@@ -279,9 +403,9 @@ export function EditGameModal({
// Helper function to prepare non-custom game assets
const prepareNonCustomGameAssets = () => {
return {
customIconUrl: iconPath ? `local:${iconPath}` : null,
customLogoImageUrl: logoPath ? `local:${logoPath}` : null,
customHeroImageUrl: heroPath ? `local:${heroPath}` : null,
customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null,
customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null,
customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null,
};
};
@@ -290,14 +414,17 @@ export function EditGameModal({
const { iconUrl, logoImageUrl, libraryHeroImageUrl } =
prepareCustomGameAssets(game);
return window.electron.updateCustomGame(
game.shop,
game.objectId,
gameName.trim(),
iconUrl || undefined,
logoImageUrl || undefined,
libraryHeroImageUrl || undefined
);
return window.electron.updateCustomGame({
shop: game.shop,
objectId: game.objectId,
title: gameName.trim(),
iconUrl: iconUrl || undefined,
logoImageUrl: logoImageUrl || undefined,
libraryHeroImageUrl: libraryHeroImageUrl || undefined,
originalIconPath: originalAssetPaths.icon || undefined,
originalLogoPath: originalAssetPaths.logo || undefined,
originalHeroPath: originalAssetPaths.hero || undefined,
});
};
// Helper function to update non-custom game
@@ -305,14 +432,17 @@ export function EditGameModal({
const { customIconUrl, customLogoImageUrl, customHeroImageUrl } =
prepareNonCustomGameAssets();
return window.electron.updateGameCustomAssets(
game.shop,
game.objectId,
gameName.trim(),
return window.electron.updateGameCustomAssets({
shop: game.shop,
objectId: game.objectId,
title: gameName.trim(),
customIconUrl,
customLogoImageUrl,
customHeroImageUrl
);
customHeroImageUrl,
customOriginalIconPath: originalAssetPaths.icon || undefined,
customOriginalLogoPath: originalAssetPaths.logo || undefined,
customOriginalHeroPath: originalAssetPaths.hero || undefined,
});
};
const handleUpdateGame = async () => {
@@ -343,19 +473,31 @@ export function EditGameModal({
};
// Helper function to reset form to initial state
const resetFormToInitialState = (game: LibraryGame | Game) => {
setGameName(game.title || "");
const resetFormToInitialState = useCallback(
(game: LibraryGame | Game) => {
setGameName(game.title || "");
if (isCustomGame(game)) {
setCustomGameAssets(game);
// Clear default URLs for custom games
setDefaultIconUrl(null);
setDefaultLogoUrl(null);
setDefaultHeroUrl(null);
} else {
setNonCustomGameAssets(game as LibraryGame);
}
};
// Reset removed assets state
setRemovedAssets({
icon: false,
logo: false,
hero: false,
});
if (isCustomGame(game)) {
setCustomGameAssets(game);
// Clear default URLs for custom games
setDefaultUrls({
icon: null,
logo: null,
hero: null,
});
} else {
setNonCustomGameAssets(game as LibraryGame);
}
},
[setCustomGameAssets, setNonCustomGameAssets]
);
const handleClose = () => {
if (!isUpdating && game) {
@@ -378,6 +520,7 @@ export function EditGameModal({
const renderImageSection = (assetType: AssetType) => {
const assetPath = getAssetPath(assetType);
const assetDisplayPath = getAssetDisplayPath(assetType);
const defaultUrl = getDefaultUrl(assetType);
const hasImage = assetPath || (game && !isCustomGame(game) && defaultUrl);
const isDragOver = dragOverTarget === assetType;
@@ -390,7 +533,7 @@ export function EditGameModal({
<div className="edit-game-modal__image-section">
<TextField
placeholder={t(`edit_game_modal_select_${assetType}`)}
value={assetPath}
value={assetDisplayPath}
readOnly
theme="dark"
rightContent={
@@ -404,17 +547,19 @@ export function EditGameModal({
<ImageIcon />
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && assetPath && (
<Button
type="button"
theme="outline"
onClick={() => handleRestoreDefault(assetType)}
disabled={isUpdating}
title={`Remove ${assetType}`}
>
<XIcon />
</Button>
)}
{game &&
(assetPath ||
(isCustomGame(game) && getOriginalAssetUrl(assetType))) && (
<Button
type="button"
theme="outline"
onClick={() => handleRestoreDefault(assetType)}
disabled={isUpdating}
title={`Remove ${assetType}`}
>
<XIcon />
</Button>
)}
</div>
}
/>
@@ -442,7 +587,7 @@ export function EditGameModal({
/>
{isDragOver && (
<div className="edit-game-modal__drop-overlay">
<span>Drop to replace {assetType}</span>
<span>{t(`edit_game_modal_drop_to_replace_${assetType}`)}</span>
</div>
)}
</button>
@@ -465,7 +610,7 @@ export function EditGameModal({
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop {assetType} image here</span>
<span>{t(`edit_game_modal_drop_${assetType}_image_here`)}</span>
</div>
</button>
)}
@@ -489,6 +634,19 @@ export function EditGameModal({
onChange={handleGameNameChange}
theme="dark"
disabled={isUpdating}
rightContent={
isTitleChanged && (
<Button
type="button"
theme="outline"
onClick={handleRestoreDefaultTitle}
disabled={isUpdating}
title="Restore default title"
>
<XIcon />
</Button>
)
}
/>
<div className="edit-game-modal__asset-selector">

View File

@@ -2,12 +2,33 @@
.repacks-modal {
&__filter-container {
transition: all 0.3s ease;
}
&__filter-top {
margin-bottom: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: row;
gap: calc(globals.$spacing-unit * 1);
align-items: center;
}
&__filter-toggle {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 1);
font-size: globals.$small-font-size;
font-weight: 600;
color: var(--color-text-secondary);
padding: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 1.5);
border-radius: 6px;
transition: background-color 0.2s ease;
white-space: nowrap;
}
&__repacks {
display: flex;
gap: globals.$spacing-unit;
gap: calc(globals.$spacing-unit * 2);
flex-direction: column;
}
@@ -16,7 +37,7 @@
text-align: left;
flex-direction: column;
align-items: flex-start;
gap: globals.$spacing-unit;
gap: calc(globals.$spacing-unit * 1);
color: globals.$body-color;
padding: calc(globals.$spacing-unit * 2);
}
@@ -29,4 +50,106 @@
&__repack-info {
font-size: globals.$small-font-size;
}
&__no-results {
width: 100%;
padding: calc(globals.$spacing-unit * 4) 0;
text-align: center;
color: globals.$muted-color;
font-size: globals.$small-font-size;
display: flex;
align-items: center;
justify-content: center;
}
&__no-results-content {
display: flex;
flex-direction: column;
align-items: center;
gap: calc(globals.$spacing-unit * 1.5);
max-width: 480px;
width: 100%;
}
&__no-results-text {
color: globals.$muted-color;
font-size: globals.$small-font-size;
text-align: center;
}
&__no-results-button {
display: flex;
justify-content: center;
width: 100%;
}
&__download-sources {
padding: 0;
background-color: var(--color-background-light);
border-radius: 8px;
margin-bottom: calc(globals.$spacing-unit * 2);
margin-top: calc(globals.$spacing-unit * 1);
max-height: 0;
overflow: hidden;
transition:
max-height 0.3s ease,
padding 0.3s ease;
&--open {
max-height: 280px;
}
}
&__filter-label {
display: none;
font-size: globals.$small-font-size;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--color-text-secondary);
width: 100%;
}
&__source-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: calc(globals.$spacing-unit * 1);
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
align-items: start;
padding: calc(globals.$spacing-unit * 0.5) calc(globals.$spacing-unit * 0.5)
calc(globals.$spacing-unit * 0.5) 0;
}
&__source-item {
padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1);
background: var(--color-surface, rgba(0, 0, 0, 0.03));
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
display: flex;
align-items: center;
min-height: calc(globals.$spacing-unit * 5);
box-sizing: border-box;
width: 100%;
transition: border-color 0.2s ease;
&:hover {
border-color: rgba(255, 255, 255, 0.2);
}
}
&__source-item :global(.checkbox-field) {
width: 100%;
min-width: 0;
}
&__source-item :global(.checkbox-field__label) {
white-space: normal;
overflow: visible;
text-overflow: unset;
display: block;
font-size: 0.85rem;
width: 100%;
word-break: break-word;
}
}

View File

@@ -1,5 +1,11 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import {
PlusCircleIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@primer/octicons-react";
import {
Badge,
@@ -7,7 +13,10 @@ import {
DebridBadge,
Modal,
TextField,
CheckboxField,
} from "@renderer/components";
import { downloadSourcesTable } from "@renderer/dexie";
import type { DownloadSource } from "@types";
import type { GameRepack } from "@types";
import { DownloadSettingsModal } from "./download-settings-modal";
@@ -36,6 +45,11 @@ export function RepacksModal({
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack | null>(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
const [selectedFingerprints, setSelectedFingerprints] = useState<string[]>(
[]
);
const [filterTerm, setFilterTerm] = useState("");
const [hashesInDebrid, setHashesInDebrid] = useState<Record<string, boolean>>(
{}
@@ -46,6 +60,7 @@ export function RepacksModal({
const { t } = useTranslation("game_details");
const { formatDate } = useDate();
const navigate = useNavigate();
const getHashFromMagnet = (magnet: string) => {
if (!magnet || typeof magnet !== "string") {
@@ -90,8 +105,37 @@ export function RepacksModal({
}, [repacks, hashesInDebrid]);
useEffect(() => {
setFilteredRepacks(sortedRepacks);
}, [sortedRepacks, visible, game]);
downloadSourcesTable.toArray().then((sources) => {
const uniqueRepackers = new Set(sortedRepacks.map((r) => r.repacker));
const filteredSources = sources.filter(
(s) => s.name && uniqueRepackers.has(s.name) && !!s.fingerprint
);
setDownloadSources(filteredSources);
});
}, [sortedRepacks]);
useEffect(() => {
const term = filterTerm.trim().toLowerCase();
const byTerm = sortedRepacks.filter((repack) => {
if (!term) return true;
const lowerTitle = repack.title.toLowerCase();
const lowerRepacker = repack.repacker.toLowerCase();
return lowerTitle.includes(term) || lowerRepacker.includes(term);
});
const bySource = byTerm.filter((repack) => {
if (selectedFingerprints.length === 0) return true;
return downloadSources.some(
(src) =>
selectedFingerprints.includes(src.fingerprint) &&
src.name === repack.repacker
);
});
setFilteredRepacks(bySource);
}, [sortedRepacks, filterTerm, selectedFingerprints, downloadSources]);
const handleRepackClick = (repack: GameRepack) => {
setRepack(repack);
@@ -99,17 +143,14 @@ export function RepacksModal({
};
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
const term = event.target.value.toLocaleLowerCase();
setFilterTerm(event.target.value);
};
setFilteredRepacks(
sortedRepacks.filter((repack) => {
const lowerCaseTitle = repack.title.toLowerCase();
const lowerCaseRepacker = repack.repacker.toLowerCase();
return [lowerCaseTitle, lowerCaseRepacker].some((value) =>
value.includes(term)
);
})
const toggleFingerprint = (fingerprint: string) => {
setSelectedFingerprints((prev) =>
prev.includes(fingerprint)
? prev.filter((f) => f !== fingerprint)
: [...prev, fingerprint]
);
};
@@ -118,6 +159,8 @@ export function RepacksModal({
return repack.uris.some((uri) => uri.includes(game.download!.uri));
};
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
return (
<>
<DownloadSettingsModal
@@ -133,38 +176,103 @@ export function RepacksModal({
description={t("repacks_modal_description")}
onClose={onClose}
>
<div className="repacks-modal__filter-container">
<TextField placeholder={t("filter")} onChange={handleFilter} />
<div
className={`repacks-modal__filter-container ${isFilterDrawerOpen ? "repacks-modal__filter-container--drawer-open" : ""}`}
>
<div className="repacks-modal__filter-top">
<TextField placeholder={t("filter")} onChange={handleFilter} />
{downloadSources.length > 0 && (
<Button
type="button"
theme="outline"
onClick={() => setIsFilterDrawerOpen(!isFilterDrawerOpen)}
className="repacks-modal__filter-toggle"
>
{t("filter_by_source")}
{isFilterDrawerOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Button>
)}
</div>
<div
className={`repacks-modal__download-sources ${isFilterDrawerOpen ? "repacks-modal__download-sources--open" : ""}`}
>
<div className="repacks-modal__source-grid">
{downloadSources.map((source) => {
const label = source.name || source.url;
const truncatedLabel =
label.length > 16 ? label.substring(0, 16) + "..." : label;
return (
<div
key={source.fingerprint}
className="repacks-modal__source-item"
>
<CheckboxField
label={truncatedLabel}
checked={selectedFingerprints.includes(
source.fingerprint
)}
onChange={() => toggleFingerprint(source.fingerprint)}
/>
</div>
);
})}
</div>
</div>
</div>
<div className="repacks-modal__repacks">
{filteredRepacks.map((repack) => {
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
{filteredRepacks.length === 0 ? (
<div className="repacks-modal__no-results">
<div className="repacks-modal__no-results-content">
<div className="repacks-modal__no-results-text">
{t("no_repacks_found")}
</div>
<div className="repacks-modal__no-results-button">
<Button
type="button"
theme="primary"
onClick={() => {
onClose();
navigate("/settings?tab=2");
}}
>
<PlusCircleIcon />
{t("add_download_source", { ns: "settings" })}
</Button>
</div>
</div>
</div>
) : (
filteredRepacks.map((repack) => {
const isLastDownloadedOption =
checkIfLastDownloadedOption(repack);
return (
<Button
key={repack.id}
theme="dark"
onClick={() => handleRepackClick(repack)}
className="repacks-modal__repack-button"
>
<p className="repacks-modal__repack-title">{repack.title}</p>
return (
<Button
key={repack.id}
theme="dark"
onClick={() => handleRepackClick(repack)}
className="repacks-modal__repack-button"
>
<p className="repacks-modal__repack-title">{repack.title}</p>
{isLastDownloadedOption && (
<Badge>{t("last_downloaded_option")}</Badge>
)}
{isLastDownloadedOption && (
<Badge>{t("last_downloaded_option")}</Badge>
)}
<p className="repacks-modal__repack-info">
{repack.fileSize} - {repack.repacker} -{" "}
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
</p>
<p className="repacks-modal__repack-info">
{repack.fileSize} - {repack.repacker} -{" "}
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
</p>
{hashesInDebrid[getHashFromMagnet(repack.uris[0]) ?? ""] && (
<DebridBadge />
)}
</Button>
);
})}
{hashesInDebrid[getHashFromMagnet(repack.uris[0]) ?? ""] && (
<DebridBadge />
)}
</Button>
);
})
)}
</div>
</Modal>
</>

View File

@@ -1,6 +1,12 @@
@use "../../../scss/globals.scss";
.sidebar-section {
background-color: globals.$background-color;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
&__button {
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
display: flex;
@@ -15,7 +21,7 @@
font-weight: bold;
&:hover {
background-color: rgba(255, 255, 255, 0.05);
background-color: rgba(255, 255, 255, 0.1);
}
&:active {
@@ -34,6 +40,7 @@
&__content {
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0, 1, 0, 1);
background-color: globals.$dark-background-color;
position: relative;
}
}

View File

@@ -25,7 +25,7 @@ export function HowLongToBeatSection({
return `${value} ${t(durationTranslation[unit])}`;
};
if (!howLongToBeatData && !isLoading) return null;
if (!howLongToBeatData || !isLoading) return null;
return (
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">

View File

@@ -1,11 +1,12 @@
@use "../../../scss/globals.scss";
.content-sidebar {
border-left: solid 1px globals.$border-color;
background-color: globals.$dark-background-color;
height: 100%;
flex-shrink: 0;
width: 280px;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 1.5);
@media (min-width: 1024px) {
width: 320px;

View File

@@ -76,6 +76,15 @@
width: 24px;
height: 24px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
&__title-flame-icon {
width: 32px;
height: 32px;
object-fit: contain;
}
&__title {

View File

@@ -158,7 +158,7 @@ export default function Home() {
<img
src={flameIconAnimated}
alt="Flame animation"
className="home__flame-icon"
className="home__title-flame-icon"
/>
</div>
)}

View File

@@ -5,7 +5,6 @@ import { useAppDispatch, useFormat } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { UserGame } from "@types";
import { LockedProfile } from "./locked-profile";
import { ReportProfile } from "../report-profile/report-profile";
import { FriendsBox } from "./friends-box";
@@ -17,8 +16,6 @@ import { useSectionCollapse } from "@renderer/hooks/use-section-collapse";
import { motion, AnimatePresence } from "framer-motion";
import {
sectionVariants,
gameCardVariants,
gameGridVariants,
chevronVariants,
GAME_STATS_ANIMATION_DURATION_IN_MS,
} from "./profile-animations";
@@ -38,8 +35,6 @@ export function ProfileContent() {
const [statsIndex, setStatsIndex] = useState(0);
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
const [prevLibraryGames, setPrevLibraryGames] = useState<UserGame[]>([]);
const [prevPinnedGames, setPrevPinnedGames] = useState<UserGame[]>([]);
const statsAnimation = useRef(-1);
const { toggleSection, isPinnedCollapsed } = useSectionCollapse();
@@ -92,27 +87,6 @@ export function ProfileContent() {
const { numberFormatter } = useFormat();
const gamesHaveChanged = (
current: UserGame[],
previous: UserGame[]
): boolean => {
if (current.length !== previous.length) return true;
return current.some(
(game, index) => game.objectId !== previous[index]?.objectId
);
};
const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames);
const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames);
useEffect(() => {
setPrevLibraryGames(libraryGames);
}, [libraryGames]);
useEffect(() => {
setPrevPinnedGames(pinnedGames);
}, [pinnedGames]);
const usersAreFriends = useMemo(() => {
return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]);
@@ -192,57 +166,22 @@ export function ProfileContent() {
exit="collapsed"
layout
>
<motion.ul
className="profile-content__games-grid"
variants={
shouldAnimatePinned ? gameGridVariants : undefined
}
initial={shouldAnimatePinned ? "hidden" : undefined}
animate={shouldAnimatePinned ? "visible" : undefined}
exit={shouldAnimatePinned ? "exit" : undefined}
key={
shouldAnimatePinned
? `pinned-${sortBy}`
: `pinned-static`
}
>
{shouldAnimatePinned ? (
<AnimatePresence mode="wait">
{pinnedGames?.map((game, index) => (
<motion.li
key={game.objectId}
variants={gameCardVariants}
initial="hidden"
animate="visible"
exit="exit"
transition={{ delay: index * 0.1 }}
style={{ listStyle: "none" }}
>
<UserLibraryGameCard
game={game}
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
/>
</motion.li>
))}
</AnimatePresence>
) : (
pinnedGames?.map((game) => (
<li
key={game.objectId}
style={{ listStyle: "none" }}
>
<UserLibraryGameCard
game={game}
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
/>
</li>
))
)}
</motion.ul>
<ul className="profile-content__games-grid">
{pinnedGames?.map((game) => (
<li
key={game.objectId}
style={{ listStyle: "none" }}
>
<UserLibraryGameCard
game={game}
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
sortBy={sortBy}
/>
</li>
))}
</ul>
</motion.div>
)}
</AnimatePresence>
@@ -262,54 +201,19 @@ export function ProfileContent() {
</div>
</div>
<motion.ul
className="profile-content__games-grid"
variants={
shouldAnimateLibrary ? gameGridVariants : undefined
}
initial={shouldAnimateLibrary ? "hidden" : undefined}
animate={shouldAnimateLibrary ? "visible" : undefined}
exit={shouldAnimateLibrary ? "exit" : undefined}
key={
shouldAnimateLibrary
? `library-${sortBy}`
: `library-static`
}
>
{shouldAnimateLibrary ? (
<AnimatePresence mode="wait">
{libraryGames?.map((game, index) => (
<motion.li
key={game.objectId}
variants={gameCardVariants}
initial="hidden"
animate="visible"
exit="exit"
transition={{ delay: index * 0.1 }}
style={{ listStyle: "none" }}
>
<UserLibraryGameCard
game={game}
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
/>
</motion.li>
))}
</AnimatePresence>
) : (
libraryGames?.map((game) => (
<li key={game.objectId} style={{ listStyle: "none" }}>
<UserLibraryGameCard
game={game}
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
/>
</li>
))
)}
</motion.ul>
<ul className="profile-content__games-grid">
{libraryGames?.map((game) => (
<li key={game.objectId} style={{ listStyle: "none" }}>
<UserLibraryGameCard
game={game}
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
sortBy={sortBy}
/>
</li>
))}
</ul>
</div>
)}
</div>
@@ -338,8 +242,6 @@ export function ProfileContent() {
pinnedGames,
isPinnedCollapsed,
toggleSection,
shouldAnimateLibrary,
shouldAnimatePinned,
sortBy,
]);

View File

@@ -8,6 +8,7 @@
display: flex;
transition: all ease 0.2s;
cursor: grab;
container-type: inline-size;
&:hover {
transform: scale(1.05);
@@ -86,32 +87,10 @@
top: 8px;
right: 8px;
display: flex;
gap: 6px;
gap: 4px;
z-index: 2;
}
&__favorite-icon {
color: rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: solid 1px rgba(255, 255, 255, 0.15);
border-radius: 50%;
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all ease 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.5);
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
}
&__pin-button {
color: rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.4);
@@ -154,11 +133,25 @@
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all ease 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.5);
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
&-long {
display: inline;
font-size: 12px;
}
&-short {
display: none;
font-size: 12px;
}
// When the card is narrow (less than 180px), show short format
@container (max-width: 140px) {
&-long {
display: none;
}
&-short {
display: inline;
}
}
}
&__manual-playtime {

View File

@@ -13,7 +13,6 @@ import {
ClockIcon,
TrophyIcon,
AlertFillIcon,
HeartFillIcon,
PinIcon,
PinSlashIcon,
} from "@primer/octicons-react";
@@ -27,6 +26,7 @@ interface UserLibraryGameCardProps {
statIndex: number;
onMouseEnter: () => void;
onMouseLeave: () => void;
sortBy?: string;
}
export function UserLibraryGameCard({
@@ -34,6 +34,7 @@ export function UserLibraryGameCard({
statIndex,
onMouseEnter,
onMouseLeave,
sortBy,
}: UserLibraryGameCardProps) {
const { userProfile, isMe, getUserLibraryGames } =
useContext(userProfileContext);
@@ -79,17 +80,22 @@ export function UserLibraryGameCard({
};
const formatPlayTime = useCallback(
(playTimeInSeconds = 0) => {
(playTimeInSeconds = 0, isShort = false) => {
const minutes = playTimeInSeconds / 60;
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
return t("amount_minutes", {
return t(isShort ? "amount_minutes_short" : "amount_minutes", {
amount: minutes.toFixed(0),
});
}
const hours = minutes / 60;
return t("amount_hours", { amount: numberFormatter.format(hours) });
const hoursKey = isShort ? "amount_hours_short" : "amount_hours";
const hoursAmount = isShort
? Math.floor(hours)
: numberFormatter.format(hours);
return t(hoursKey, { amount: hoursAmount });
},
[numberFormatter, t]
);
@@ -104,7 +110,7 @@ export function UserLibraryGameCard({
!game.isPinned
);
await getUserLibraryGames();
await getUserLibraryGames(sortBy);
if (game.isPinned) {
showSuccessToast(t("game_removed_from_pinned"));
@@ -130,33 +136,26 @@ export function UserLibraryGameCard({
onClick={() => navigate(buildUserGameDetailsPath(game))}
>
<div className="user-library-game__overlay">
{(game.isFavorite || isMe) && (
{isMe && (
<div className="user-library-game__actions-container">
{game.isFavorite && (
<div className="user-library-game__favorite-icon">
<HeartFillIcon size={12} />
</div>
)}
{isMe && (
<button
type="button"
className="user-library-game__pin-button"
onClick={(e) => {
e.stopPropagation();
toggleGamePinned();
}}
disabled={isPinning}
>
{game.isPinned ? (
<PinSlashIcon size={12} />
) : (
<PinIcon size={12} />
)}
</button>
)}
<button
type="button"
className="user-library-game__pin-button"
onClick={(e) => {
e.stopPropagation();
toggleGamePinned();
}}
disabled={isPinning}
>
{game.isPinned ? (
<PinSlashIcon size={12} />
) : (
<PinIcon size={12} />
)}
</button>
</div>
)}
<small
<div
className="user-library-game__playtime"
data-tooltip-place="top"
data-tooltip-content={
@@ -174,8 +173,13 @@ export function UserLibraryGameCard({
) : (
<ClockIcon size={11} />
)}
{formatPlayTime(game.playTimeInSeconds)}
</small>
<span className="user-library-game__playtime-long">
{formatPlayTime(game.playTimeInSeconds)}
</span>
<span className="user-library-game__playtime-short">
{formatPlayTime(game.playTimeInSeconds, true)}
</span>
</div>
{userProfile?.hasActiveSubscription &&
game.achievementCount > 0 && (

View File

@@ -38,6 +38,12 @@ export interface Game {
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
originalIconPath?: string | null;
originalLogoPath?: string | null;
originalHeroPath?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
playTimeInMilliseconds: number;
unsyncedDeltaPlayTimeInMilliseconds?: number;
lastTimePlayed: Date | null;