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 4340e41a..55c43781 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -508,6 +508,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..efed5e2d 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -1,7 +1,6 @@ { "language_name": "Magyar", "home": { - "featured": "Featured", "surprise_me": "Lepj meg", "no_results": "Nem található" }, @@ -19,7 +18,6 @@ }, "header": { "search": "Keresés", - "home": "Főoldal", "catalogue": "Katalógus", "downloads": "Letöltések", @@ -31,10 +29,7 @@ "downloading_metadata": "{{title}} metaadatainak letöltése…", "downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "Következő olda", - "previous_page": "Előző olda" - }, + "catalogue": {}, "game_details": { "open_download_options": "Letöltési lehetőségek", "download_options_zero": "Nincs letöltési lehetőség", diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json index 4fa347fc..fc72fc51 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 99e6e8c4..6dac122f 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", @@ -222,6 +237,17 @@ "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", @@ -396,7 +422,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", @@ -525,7 +552,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..03413554 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": "Каталог", @@ -486,6 +484,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/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/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/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..0f6df95d 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,4 +1,4 @@ -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"; @@ -29,16 +29,29 @@ 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 [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 +61,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,39 +138,24 @@ 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 => { - switch (assetType) { - case "icon": - return defaultIconUrl; - case "logo": - return defaultLogoUrl; - case "hero": - return defaultHeroUrl; - } + return defaultUrls[assetType]; }; const handleSelectAsset = async (assetType: AssetType) => { @@ -140,23 +170,55 @@ 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, + })); } 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, + })); } } }; const handleRestoreDefault = (assetType: AssetType) => { 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 +294,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,10 +330,12 @@ 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}` + const iconUrl = assetPaths.icon ? `local:${assetPaths.icon}` : game.iconUrl; + const logoImageUrl = assetPaths.logo + ? `local:${assetPaths.logo}` + : game.logoImageUrl; + const libraryHeroImageUrl = assetPaths.hero + ? `local:${assetPaths.hero}` : game.libraryHeroImageUrl; return { iconUrl, logoImageUrl, libraryHeroImageUrl }; @@ -279,9 +344,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 +355,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 +373,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 +414,24 @@ 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); - } - }; + 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 +454,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 +467,7 @@ export function EditGameModal({
+ + + ) + } />
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..5d0d7f2c 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); @@ -160,6 +139,27 @@ 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 { color: globals.$warning-color; 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;