diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c54c431c..0f3e0a66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2536c33..babfb565 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json index 782d6b51..034d0cbd 100644 --- a/src/locales/ar/translation.json +++ b/src/locales/ar/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "تم تسجيل الدخول بنجاح" }, "home": { - "featured": "مميز", "surprise_me": "مفاجئني", "no_results": "لم يتم العثور على نتائج", "start_typing": "ابدأ بالكتابة للبحث...", diff --git a/src/locales/be/translation.json b/src/locales/be/translation.json index c9d49626..8d67e693 100644 --- a/src/locales/be/translation.json +++ b/src/locales/be/translation.json @@ -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": "Няма варыянтаў сцягвання", diff --git a/src/locales/bg/translation.json b/src/locales/bg/translation.json index 458b9e36..3e289700 100644 --- a/src/locales/bg/translation.json +++ b/src/locales/bg/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Успешно влизане" }, "home": { - "featured": "Препоръчани", "surprise_me": "Изненадай ме", "no_results": "Няма намерени резултати", "start_typing": "Започнете да пишете за търсене...", diff --git a/src/locales/ca/translation.json b/src/locales/ca/translation.json index aa69001f..96eb67e2 100644 --- a/src/locales/ca/translation.json +++ b/src/locales/ca/translation.json @@ -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", diff --git a/src/locales/cs/translation.json b/src/locales/cs/translation.json index 9b501b54..6bcc8944 100644 --- a/src/locales/cs/translation.json +++ b/src/locales/cs/translation.json @@ -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í...", diff --git a/src/locales/da/translation.json b/src/locales/da/translation.json index 618f085c..21a92f72 100644 --- a/src/locales/da/translation.json +++ b/src/locales/da/translation.json @@ -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", diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 5101f459..fb285ee0 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -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", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ae5411c1..ba8e9801 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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", diff --git a/src/locales/et/translation.json b/src/locales/et/translation.json index 119e1aab..c5566eeb 100644 --- a/src/locales/et/translation.json +++ b/src/locales/et/translation.json @@ -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", diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json index be18263a..69a49b79 100644 --- a/src/locales/fa/translation.json +++ b/src/locales/fa/translation.json @@ -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": "هیچ آپشن دانلودی وجود ندارد", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 1c129a64..8fc07722 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -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...", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 0cea87b0..37ddb78f 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -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 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 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 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" } } diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json index d6d591b6..b782da21 100644 --- a/src/locales/id/translation.json +++ b/src/locales/id/translation.json @@ -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", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index b23d1244..ac37ffe9 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -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", diff --git a/src/locales/kk/translation.json b/src/locales/kk/translation.json index bfb009a7..48fb8181 100644 --- a/src/locales/kk/translation.json +++ b/src/locales/kk/translation.json @@ -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": "Жүктеу нұсқалары жоқ", diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 9ec389b1..a9b9c0e5 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -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": "다운로드 선택지 없음", diff --git a/src/locales/nb/translation.json b/src/locales/nb/translation.json index 8898ec7b..95bda8fe 100644 --- a/src/locales/nb/translation.json +++ b/src/locales/nb/translation.json @@ -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", diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json index 72d20c74..baa6df6e 100644 --- a/src/locales/nl/translation.json +++ b/src/locales/nl/translation.json @@ -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", diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json index 86751b0e..2e0d1696 100644 --- a/src/locales/pl/translation.json +++ b/src/locales/pl/translation.json @@ -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", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 5032208f..37569701 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -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", diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 6c32b35b..654e94ec 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -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", - "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", diff --git a/src/locales/ro/translation.json b/src/locales/ro/translation.json index c5a81881..8ed6fd39 100644 --- a/src/locales/ro/translation.json +++ b/src/locales/ro/translation.json @@ -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", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 58235989..8992a4a0 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -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": "Библиотека", diff --git a/src/locales/sv/translation.json b/src/locales/sv/translation.json index 0972effa..901e4ca7 100644 --- a/src/locales/sv/translation.json +++ b/src/locales/sv/translation.json @@ -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...", diff --git a/src/locales/tr/translation.json b/src/locales/tr/translation.json index c3fe2081..e8e1cb2b 100644 --- a/src/locales/tr/translation.json +++ b/src/locales/tr/translation.json @@ -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...", diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json index 48a3972d..26aa8aae 100644 --- a/src/locales/uk/translation.json +++ b/src/locales/uk/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Успішний вхід в систему" }, "home": { - "featured": "Рекомендоване", "surprise_me": "Здивуй мене", "no_results": "Результатів не знайдено", "start_typing": "Почніть набирати текст для пошуку...", diff --git a/src/locales/uz/translation.json b/src/locales/uz/translation.json index d20a9677..24e508af 100644 --- a/src/locales/uz/translation.json +++ b/src/locales/uz/translation.json @@ -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", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 0323d991..7cdd0c92 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -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": "下载源", diff --git a/src/main/events/catalogue/get-game-stats.ts b/src/main/events/catalogue/get-game-stats.ts index 559d3a7d..b836531d 100644 --- a/src/main/events/catalogue/get-game-stats.ts +++ b/src/main/events/catalogue/get-game-stats.ts @@ -21,11 +21,9 @@ const getGameStats = async ( return cachedStats; } - return HydraApi.get( - `/games/stats`, - { objectId, shop }, - { needsAuth: false } - ).then(async (data) => { + return HydraApi.get(`/games/${shop}/${objectId}/stats`, null, { + needsAuth: false, + }).then(async (data) => { await gamesStatsCacheSublevel.put(levelKeys.game(shop, objectId), { ...data, updatedAt: Date.now(), diff --git a/src/main/events/catalogue/get-how-long-to-beat.ts b/src/main/events/catalogue/get-how-long-to-beat.ts index 2a1492ef..0d630164 100644 --- a/src/main/events/catalogue/get-how-long-to-beat.ts +++ b/src/main/events/catalogue/get-how-long-to-beat.ts @@ -8,12 +8,7 @@ const getHowLongToBeat = async ( objectId: string, shop: GameShop ): Promise => { - 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, }); }; diff --git a/src/main/events/catalogue/get-trending-games.ts b/src/main/events/catalogue/get-trending-games.ts index 98b09136..4c587f37 100644 --- a/src/main/events/catalogue/get-trending-games.ts +++ b/src/main/events/catalogue/get-trending-games.ts @@ -11,7 +11,7 @@ const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => { .then((language) => language || "en"); const trendingGames = await HydraApi.get( - "/games/featured", + "/catalogue/featured", { language }, { needsAuth: false } ).catch(() => []); diff --git a/src/main/events/library/create-steam-shortcut.ts b/src/main/events/library/create-steam-shortcut.ts index b12d503f..f83dd675 100644 --- a/src/main/events/library/create-steam-shortcut.ts +++ b/src/main/events/library/create-steam-shortcut.ts @@ -87,7 +87,7 @@ const createSteamShortcut = async ( } const { assets } = await HydraApi.get( - `/games/stats?objectId=${objectId}&shop=${shop}` + `/games/${shop}/${objectId}/stats` ); const steamUserIds = await getSteamUsersIds(); diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 6a33ffaf..fbb60ab2 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -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 => { + 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 => { + 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 => { + 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); diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 6152c0df..8129fc57 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -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; }; diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 866cd60e..1f912901 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -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 => { + 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 => { + const existingAssets = await gamesShopAssetsSublevel.get(gameKey); + if (existingAssets) { + const updatedAssets = { + ...existingAssets, + title, + }; + await gamesShopAssetsSublevel.put(gameKey, updatedAssets); + } +}; + +const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise => { + 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; }; diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 8216b519..79d55ec3 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -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 }; diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index e2b663d8..72b49bc5 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -45,10 +45,8 @@ export const getGameAchievementData = async ( .then((language) => language || "en"); return HydraApi.get( - "/games/achievements", + `/games/${shop}/${objectId}/achievements`, { - shop, - objectId, language, }, { diff --git a/src/main/services/steam-250.ts b/src/main/services/steam-250.ts index 0abc2f14..5652b0d3 100644 --- a/src/main/services/steam-250.ts +++ b/src/main/services/steam-250.ts @@ -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; diff --git a/src/main/services/ws/events/friend-game-session.ts b/src/main/services/ws/events/friend-game-session.ts index 47d8164e..67967b3c 100644 --- a/src/main/services/ws/events/friend-game-session.ts +++ b/src/main/services/ws/events/friend-game-session.ts @@ -16,9 +16,7 @@ export const friendGameSessionEvent = async (payload: FriendGameSession) => { const [friend, gameStats] = await Promise.all([ HydraApi.get(`/users/${payload.friendId}`), - HydraApi.get( - `/games/stats?objectId=${payload.objectId}&shop=steam` - ), + HydraApi.get(`/games/steam/${payload.objectId}/stats`), ]).catch(() => [null, null]); if (friend && gameStats) { diff --git a/src/preload/index.ts b/src/preload/index.ts index e536f8c7..17c1225f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 81d18940..e6277888 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -119,14 +119,17 @@ declare global { logoImageUrl?: string, libraryHeroImageUrl?: string ) => Promise; - updateCustomGame: ( - shop: GameShop, - objectId: string, - title: string, - iconUrl?: string, - logoImageUrl?: string, - libraryHeroImageUrl?: string - ) => Promise; + updateCustomGame: (params: { + shop: GameShop; + objectId: string; + title: string; + iconUrl?: string; + logoImageUrl?: string; + libraryHeroImageUrl?: string; + originalIconPath?: string; + originalLogoPath?: string; + originalHeroPath?: string; + }) => Promise; 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; + 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; createGameShortcut: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/pages/game-details/description-header/description-header.scss b/src/renderer/src/pages/game-details/description-header/description-header.scss index 920e8068..1af1480d 100644 --- a/src/renderer/src/pages/game-details/description-header/description-header.scss +++ b/src/renderer/src/pages/game-details/description-header/description-header.scss @@ -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; diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss index f66da32b..9483b50e 100644 --- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss @@ -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; diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 347e5a1c..ca2ca023 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -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()}
- + {game && ( + + )} {game?.shop !== "custom" && (
); } diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 786a8d30..e1140d31 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -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 { diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 04a27779..a31ce400 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -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("icon"); - const [defaultIconUrl, setDefaultIconUrl] = useState(null); - const [defaultLogoUrl, setDefaultLogoUrl] = useState(null); - const [defaultHeroUrl, setDefaultHeroUrl] = useState(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(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({
{t("edit_game_modal_browse")} - {game && !isCustomGame(game) && assetPath && ( - - )} + {game && + (assetPath || + (isCustomGame(game) && getOriginalAssetUrl(assetType))) && ( + + )}
} /> @@ -442,7 +587,7 @@ export function EditGameModal({ /> {isDragOver && (
- Drop to replace {assetType} + {t(`edit_game_modal_drop_to_replace_${assetType}`)}
)} @@ -465,7 +610,7 @@ export function EditGameModal({ >
- Drop {assetType} image here + {t(`edit_game_modal_drop_${assetType}_image_here`)}
)} @@ -489,6 +634,19 @@ export function EditGameModal({ onChange={handleGameNameChange} theme="dark" disabled={isUpdating} + rightContent={ + isTitleChanged && ( + + ) + } />
diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.scss b/src/renderer/src/pages/game-details/modals/repacks-modal.scss index 2eb2512d..ba9778fd 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.scss +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.scss @@ -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; + } } diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index ad45a73f..ec7dc3f8 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -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([]); const [repack, setRepack] = useState(null); const [showSelectFolderModal, setShowSelectFolderModal] = useState(false); + const [downloadSources, setDownloadSources] = useState([]); + const [selectedFingerprints, setSelectedFingerprints] = useState( + [] + ); + const [filterTerm, setFilterTerm] = useState(""); const [hashesInDebrid, setHashesInDebrid] = useState>( {} @@ -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 = (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 ( <> -
- +
+
+ + {downloadSources.length > 0 && ( + + )} +
+ +
+
+ {downloadSources.map((source) => { + const label = source.name || source.url; + const truncatedLabel = + label.length > 16 ? label.substring(0, 16) + "..." : label; + return ( +
+ toggleFingerprint(source.fingerprint)} + /> +
+ ); + })} +
+
- {filteredRepacks.map((repack) => { - const isLastDownloadedOption = checkIfLastDownloadedOption(repack); + {filteredRepacks.length === 0 ? ( +
+
+
+ {t("no_repacks_found")} +
+
+ +
+
+
+ ) : ( + filteredRepacks.map((repack) => { + const isLastDownloadedOption = + checkIfLastDownloadedOption(repack); - return ( - - ); - })} + {hashesInDebrid[getHashFromMagnet(repack.uris[0]) ?? ""] && ( + + )} + + ); + }) + )}
diff --git a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss index 8674b044..69083f14 100644 --- a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss +++ b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss @@ -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; } } diff --git a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx index 61c90389..9a29f150 100644 --- a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx +++ b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx @@ -25,7 +25,7 @@ export function HowLongToBeatSection({ return `${value} ${t(durationTranslation[unit])}`; }; - if (!howLongToBeatData && !isLoading) return null; + if (!howLongToBeatData || !isLoading) return null; return ( diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.scss b/src/renderer/src/pages/game-details/sidebar/sidebar.scss index d1c54f84..06519f6c 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.scss +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.scss @@ -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; diff --git a/src/renderer/src/pages/home/home.scss b/src/renderer/src/pages/home/home.scss index 497f074e..478e96a1 100644 --- a/src/renderer/src/pages/home/home.scss +++ b/src/renderer/src/pages/home/home.scss @@ -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 { diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index e2f66283..0b762882 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -158,7 +158,7 @@ export default function Home() { Flame animation
)} diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 8de16d3d..56f7d20b 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -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("playedRecently"); - const [prevLibraryGames, setPrevLibraryGames] = useState([]); - const [prevPinnedGames, setPrevPinnedGames] = useState([]); 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 > - - {shouldAnimatePinned ? ( - - {pinnedGames?.map((game, index) => ( - - - - ))} - - ) : ( - pinnedGames?.map((game) => ( -
  • - -
  • - )) - )} -
    +
      + {pinnedGames?.map((game) => ( +
    • + +
    • + ))} +
    )} @@ -262,54 +201,19 @@ export function ProfileContent() {
    - - {shouldAnimateLibrary ? ( - - {libraryGames?.map((game, index) => ( - - - - ))} - - ) : ( - libraryGames?.map((game) => ( -
  • - -
  • - )) - )} -
    +
      + {libraryGames?.map((game) => ( +
    • + +
    • + ))} +
    )} @@ -338,8 +242,6 @@ export function ProfileContent() { pinnedGames, isPinnedCollapsed, toggleSection, - shouldAnimateLibrary, - shouldAnimatePinned, sortBy, ]); diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index f072fdd5..61640536 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -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 { diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index 860c6758..a3d24958 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -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))} >
    - {(game.isFavorite || isMe) && ( + {isMe && (
    - {game.isFavorite && ( -
    - -
    - )} - {isMe && ( - - )} +
    )} - )} - {formatPlayTime(game.playTimeInSeconds)} - + + {formatPlayTime(game.playTimeInSeconds)} + + + {formatPlayTime(game.playTimeInSeconds, true)} + +
    {userProfile?.hasActiveSubscription && game.achievementCount > 0 && ( diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 73fce370..8a6c56a0 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -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;