diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json index 156c3da4..a4724b21 100644 --- a/src/locales/ar/translation.json +++ b/src/locales/ar/translation.json @@ -309,7 +309,7 @@ "last_time_played": "لعبت آخر مرة {{period}}", "activity": "النشاط الأخير", "library": "مكتبة", - "total_play_time": "إجمالي وقت اللعب: {{amount}}", + "total_play_time": "إجمالي وقت اللعب", "no_recent_activity_title": "هممم... لا شيء هنا", "no_recent_activity_description": "لم تلعب أي مباراة مؤخرًا. ", "display_name": "اسم العرض", @@ -383,13 +383,13 @@ "achievement_unlocked": "تم فتح الإنجاز", "user_achievements": "{{displayName}}إنجازات", "your_achievements": "إنجازاتك", - "unlocked_at": "مقفلة في:", + "unlocked_at": "مقفلة في: {{date}}", "subscription_needed": "مطلوب اشتراك Hydra Cloud لرؤية هذا المحتوى", "new_achievements_unlocked": "مفتوح {{achievementCount}} انجازات جديدة من {{gameCount}} ألعاب", "achievement_progress": "{{unlockedCount}}/{{totalCount}} الإنجازات", "achievements_unlocked_for_game": "مفتوح {{achievementCount}} انجازات جديدة ل {{gameTitle}}" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "اشتراك Hydra كلاود", "subscribe_now": "اشترك الآن", "cloud_saving": "الحفظ السحابي", diff --git a/src/locales/bg/translation.json b/src/locales/bg/translation.json index a1a5306f..3112bb08 100644 --- a/src/locales/bg/translation.json +++ b/src/locales/bg/translation.json @@ -293,7 +293,7 @@ "last_time_played": "Последно играно {{period}}", "activity": "Скорошна активност", "library": "Библиотека", - "total_play_time": "Общо време за игра: {{amount}}", + "total_play_time": "Общо време за игра", "no_recent_activity_title": "Хмм… няма нищо тук", "no_recent_activity_description": "Не сте играли игри напоследък. Време е да промените това.!", "display_name": "Показване на името", @@ -362,13 +362,13 @@ "achievement_unlocked": "Постижението е отключено", "user_achievements": "Постиженията на {{displayName}} ", "your_achievements": "Вашите Постижения", - "unlocked_at": "Отключено на:", + "unlocked_at": "Отключено на: {{date}}", "subscription_needed": "Необходим е абонамент за Hydra Cloud, за да видите това съдържание", "new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игра", "achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения", "achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Hydra Cloud Абонамент", "subscribe_now": "Абонирай се сега", "cloud_saving": "Запазване в облака", diff --git a/src/locales/ca/translation.json b/src/locales/ca/translation.json index 8b96486f..acf4b3c7 100644 --- a/src/locales/ca/translation.json +++ b/src/locales/ca/translation.json @@ -224,7 +224,7 @@ "last_time_played": "Última partida {{period}}", "activity": "Activitat recent", "library": "Biblioteca", - "total_play_time": "Temps total de joc:{{amount}}", + "total_play_time": "Temps total de joc", "no_recent_activity_title": "Hmmm… encara no res", "no_recent_activity_description": "No has jugat a cap joc recentment. És el moment de canviar-ho!", "display_name": "Nom de visualització", diff --git a/src/locales/cs/translation.json b/src/locales/cs/translation.json index d839fa46..c1291444 100644 --- a/src/locales/cs/translation.json +++ b/src/locales/cs/translation.json @@ -293,7 +293,7 @@ "last_time_played": "Naposledy hráno {{period}}", "activity": "Nedávná aktivita", "library": "Knihovna", - "total_play_time": "Celkový odehraný čas: {{amount}}", + "total_play_time": "Celkový odehraný čas", "no_recent_activity_title": "Hmmm… nic tu není", "no_recent_activity_description": "V poslední době si nehrál žádnout hru, můžeš to ale napravit!", "display_name": "Zobrazované jméno", @@ -362,13 +362,13 @@ "achievement_unlocked": "Achievement odemčen", "user_achievements": "Achievementy uživatele {{displayName}}", "your_achievements": "Vaše achievementy", - "unlocked_at": "Odemčeno:", + "unlocked_at": "Odemčeno: {{date}}", "subscription_needed": "Je vyžadováno předplatné Hydra Cloud pro zobrazení tohoto obsahu", "new_achievements_unlocked": "Odemčeno {{achievementCount}} nových achievementů z {{gameCount}} her", "achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementů", "achievements_unlocked_for_game": "Odemčeno {{achievementCount}} nových achievementů pro {{gameTitle}}" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Předplatné Hydra Cloud", "subscribe_now": "Připojit se", "cloud_saving": "Ukládání v cloudu", diff --git a/src/locales/da/translation.json b/src/locales/da/translation.json index 58087bfa..711c81a3 100644 --- a/src/locales/da/translation.json +++ b/src/locales/da/translation.json @@ -251,7 +251,7 @@ "last_time_played": "Sidst spillet {{period}}", "activity": "Seneste aktivitet", "library": "Bibliotek", - "total_play_time": "Samlet spiltid: {{amount}}", + "total_play_time": "Samlet spiltid", "no_recent_activity_title": "Hmmm… ikke noget her", "no_recent_activity_description": "Du har ikke spillet nogen spil for nyligt. Dét er det på tide at lave om på!", "display_name": "Brugernavn", diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 2852430f..bf1eff60 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -224,7 +224,7 @@ "last_time_played": "Zuletzt gespielt {{period}}", "activity": "Letzte Aktivität", "library": "Bibliothek", - "total_play_time": "Gesamtspielzeit: {{amount}}", + "total_play_time": "Gesamtspielzeit", "no_recent_activity_title": "Hmmm… hier ist nichts", "no_recent_activity_description": "Du hast in letzter Zeit keine Spiele gespielt. Es wird Zeit das zu ändern!", "display_name": "Anzeigename", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f74cd8e3..c0d2d545 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -163,7 +163,7 @@ "no_download_option_info": "No information available", "backup_deletion_failed": "Failed to delete backup", "max_number_of_artifacts_reached": "Maximum number of backups reached for this game", - "achievements_not_sync": "Your achievements are not synchronized", + "achievements_not_sync": "See how to synchronize your achievements", "manage_files_description": "Manage which files will be backed up and restored", "select_folder": "Select folder", "backup_from": "Backup from {{date}}", @@ -301,7 +301,7 @@ "last_time_played": "Last played {{period}}", "activity": "Recent Activity", "library": "Library", - "total_play_time": "Total playtime: {{amount}}", + "total_play_time": "Total playtime", "no_recent_activity_title": "Hmmm… nothing here", "no_recent_activity_description": "You haven't played any games recently. It's time to change that!", "display_name": "Display name", @@ -364,19 +364,34 @@ "your_friend_code": "Your friend code:", "upload_banner": "Upload banner", "uploading_banner": "Uploading banner…", - "background_image_updated": "Background image updated" + "background_image_updated": "Background image updated", + "stats": "Stats", + "achievements": "achievements", + "games": "Games", + "top_percentile": "Top {{percentile}}%", + "ranking_updated_weekly": "Ranking is updated weekly", + "playing": "Playing {{game}}", + "achievements_unlocked": "Achievements Unlocked", + "earned_points": "Earned points", + "show_achievements_on_profile": "Show your achievements on your profile", + "show_points_on_profile": "Show your earned points on your profile" }, "achievement": { "achievement_unlocked": "Achievement unlocked", "user_achievements": "{{displayName}}'s Achievements", "your_achievements": "Your Achievements", - "unlocked_at": "Unlocked at:", + "unlocked_at": "Unlocked at: {{date}}", "subscription_needed": "A Hydra Cloud subscription is required to see this content", "new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games", "achievement_progress": "{{unlockedCount}}/{{totalCount}} achievements", - "achievements_unlocked_for_game": "Unlocked {{achievementCount}} new achievements for {{gameTitle}}" + "achievements_unlocked_for_game": "Unlocked {{achievementCount}} new achievements for {{gameTitle}}", + "hidden_achievement_tooltip": "This is a hidden achievement", + "achievement_earn_points": "Earn {{points}} points with this achievement", + "earned_points": "Earned points:", + "available_points": "Available points:", + "how_to_earn_achievements_points": "How to earn achievements points?" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Hydra Cloud Subscription", "subscribe_now": "Subscribe now", "cloud_saving": "Cloud saving", @@ -384,6 +399,9 @@ "animated_profile_picture": "Animated profile pictures", "premium_support": "Premium Support", "show_and_compare_achievements": "Show and compare your achievements to other users", - "animated_profile_banner": "Animated profile banner" + "animated_profile_banner": "Animated profile banner", + "hydra_cloud": "Hydra Cloud", + "hydra_cloud_feature_found": "You've just discovered a Hydra Cloud feature!", + "learn_more": "Learn More" } } diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index a766be1c..a9261c9d 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -295,7 +295,7 @@ "last_time_played": "Última vez jugado: {{period}}", "activity": "Actividad reciente", "library": "Biblioteca", - "total_play_time": "Total de tiempo jugado: {{amount}}", + "total_play_time": "Has jugado", "no_recent_activity_title": "Que raro, no hay nada por acá...", "no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!", "display_name": "Nombre en pantalla", @@ -358,19 +358,20 @@ "your_friend_code": "Tu código de amigo:", "upload_banner": "Subir un banner", "uploading_banner": "Subiendo banner…", - "background_image_updated": "Imagen de fondo actualizada" + "background_image_updated": "Imagen de fondo actualizada", + "playing": "Jugando {{game}}" }, "achievement": { "achievement_unlocked": "Logro desbloqueado", "user_achievements": "Logros de {{displayName}}", "your_achievements": "Tus Logros", - "unlocked_at": "Desbloqueado el:", + "unlocked_at": "Desbloqueado el: {{date}}", "subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido", "new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos", "achievement_progress": "{{unlockedCount}}/{{totalCount}} logros", "achievements_unlocked_for_game": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Suscripción Hydra Cloud", "subscribe_now": "Suscribirse ahora", "cloud_saving": "Guardado en la nube", diff --git a/src/locales/et/translation.json b/src/locales/et/translation.json index 9a01fd50..91b4a63a 100644 --- a/src/locales/et/translation.json +++ b/src/locales/et/translation.json @@ -290,7 +290,7 @@ "last_time_played": "Viimati mängitud {{period}}", "activity": "Hiljutine aktiivsus", "library": "Kogu", - "total_play_time": "Kogu mängitud aeg: {{amount}}", + "total_play_time": "Kogu mängitud aeg", "no_recent_activity_title": "Hmmm… siin pole midagi", "no_recent_activity_description": "Sa pole hiljuti ühtegi mängu mänginud. On aeg seda muuta!", "display_name": "Kuvatav nimi", @@ -359,11 +359,11 @@ "achievement_unlocked": "Saavutus avatud", "user_achievements": "{{displayName}} saavutused", "your_achievements": "Sinu saavutused", - "unlocked_at": "Avatud:", + "unlocked_at": "Avatud: {{date}}", "subscription_needed": "Selle sisu nägemiseks on vaja Hydra Cloud tellimust", "new_achievements_unlocked": "Avatud {{achievementCount}} uut saavutust {{gameCount}} mängust" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Hydra Cloud Tellimus", "subscribe_now": "Telli kohe", "cloud_saving": "Pilvesalvestus", diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json index 90e32062..ba4a06f1 100644 --- a/src/locales/id/translation.json +++ b/src/locales/id/translation.json @@ -224,7 +224,7 @@ "last_time_played": "Terakhir dimainkan {{period}}", "activity": "Aktivitas terbaru", "library": "Perpustakaan", - "total_play_time": "Total waktu bermain: {{amount}}", + "total_play_time": "Total waktu bermain", "no_recent_activity_title": "Hmm… kosong di sini", "no_recent_activity_description": "Kamu belum main game baru-baru ini. Yuk, mulai main!", "display_name": "Nama tampilan", diff --git a/src/locales/kk/translation.json b/src/locales/kk/translation.json index a15f6418..6d5d8404 100644 --- a/src/locales/kk/translation.json +++ b/src/locales/kk/translation.json @@ -220,7 +220,7 @@ "last_time_played": "Соңғы ойын {{period}}", "activity": "Соңғы әрекет", "library": "Кітапхана", - "total_play_time": "Барлығы ойнаған: {{amount}}", + "total_play_time": "Барлығы ойнаған", "no_recent_activity_title": "Хммм... Мұнда ештеңе жоқ", "no_recent_activity_description": "Сіз ұзақ уақыт бойы ештеңе ойнаған жоқсыз. Мұны өзгерту керек!", "display_name": "Көрсету аты", diff --git a/src/locales/nb/translation.json b/src/locales/nb/translation.json index 32c2943b..5c5f6882 100644 --- a/src/locales/nb/translation.json +++ b/src/locales/nb/translation.json @@ -251,7 +251,7 @@ "last_time_played": "Sist spilt {{period}}", "activity": "Seneste aktivitet", "library": "Bibliotek", - "total_play_time": "Samlet spilltid: {{amount}}", + "total_play_time": "Samlet spilltid", "no_recent_activity_title": "Hmmm… ikke noe her", "no_recent_activity_description": "Du har ikke spilt noen spill for på det seneste. Det er det på tide at endre på!", "display_name": "Brukernavn", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 332e6e7d..de33fd14 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -158,7 +158,7 @@ "no_download_option_info": "Sem informações disponíveis", "backup_deletion_failed": "Falha ao apagar backup", "max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo", - "achievements_not_sync": "Suas conquistas não estão sincronizadas", + "achievements_not_sync": "Veja como exibir suas conquistas no perfil", "backup_from": "Backup de {{date}}", "custom_backup_location_set": "Localização customizada selecionada", "select_folder": "Selecione a pasta", @@ -302,7 +302,7 @@ "last_time_played": "Última sessão {{period}}", "activity": "Atividades recentes", "library": "Biblioteca", - "total_play_time": "Tempo total de jogo: {{amount}}", + "total_play_time": "Tempo total de jogo", "no_recent_activity_title": "Hmmm… nada por aqui", "no_recent_activity_description": "Parece que você não jogou nada recentemente. Que tal começar agora?", "display_name": "Nome de exibição", @@ -365,26 +365,43 @@ "your_friend_code": "Seu código de amigo:", "upload_banner": "Carregar banner", "uploading_banner": "Carregando banner…", - "background_image_updated": "Imagem de fundo salva" + "background_image_updated": "Imagem de fundo salva", + "stats": "Estatísticas", + "achievements": "conquistas", + "games": "Jogos", + "ranking_updated_weekly": "O ranking é atualizado semanalmente", + "playing": "Jogando {{game}}", + "achievements_unlocked": "Conquistas desbloqueadas", + "earned_points": "Pontos ganhos", + "show_achievements_on_profile": "Exiba suas conquistas no perfil", + "show_points_on_profile": "Exiba seus pontos ganhos no perfil" }, "achievement": { "achievement_unlocked": "Conquista desbloqueada", "your_achievements": "Suas Conquistas", "user_achievements": "Conquistas de {{displayName}}", - "unlocked_at": "Desbloqueado em:", + "unlocked_at": "Desbloqueada em: {{date}}", "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo", "new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos", "achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas", - "achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}" + "achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}", + "hidden_achievement_tooltip": "Está é uma conquista oculta", + "achievement_earn_points": "Ganhe {{points}} pontos com essa conquista", + "earned_points": "Pontos ganhos:", + "available_points": "Pontos disponíveis:", + "how_to_earn_achievements_points": "Como desbloquear pontos nas conquistas?" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Assinatura Hydra Cloud", + "hydra_cloud": "Hydra Cloud", "subscribe_now": "Inscreva-se agora", "cloud_achievements": "Salvamento de conquistas em nuvem", "animated_profile_picture": "Fotos de perfil animadas", "premium_support": "Suporte Premium", "show_and_compare_achievements": "Exiba e compare suas conquistas com outros usuários", "animated_profile_banner": "Banner animado no perfil", - "cloud_saving": "Saves de jogos em nuvem" + "cloud_saving": "Saves de jogos em nuvem", + "hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!", + "learn_more": "Saiba mais" } } diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 36320dc8..ce081b3f 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -287,7 +287,7 @@ "last_time_played": "Última sessão {{period}}", "activity": "Atividade recente", "library": "Biblioteca", - "total_play_time": "Tempo total de jogo: {{amount}}", + "total_play_time": "Tempo total de jogo", "no_recent_activity_title": "Hmmm… não há nada por aqui", "no_recent_activity_description": "Parece que não jogaste nada recentemente. Que tal começar agora?", "display_name": "Nome de apresentação", @@ -356,11 +356,11 @@ "achievement_unlocked": "Conquista desbloqueada", "your_achievements": "As tuas Conquistas", "user_achievements": "Conquistas de {{displayName}}", - "unlocked_at": "Desbloqueada em:", + "unlocked_at": "Desbloqueada em: {{date}}", "subscription_needed": "Precisas de uma subscrição Hydra Cloud para visualizar este conteúdo", "new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Subscrição Hydra Cloud", "subscribe_now": "Subscreve agora", "cloud_achievements": "Gravação de conquistas na nuvem", diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json index 8550d425..ed4b3d58 100644 --- a/src/locales/uk/translation.json +++ b/src/locales/uk/translation.json @@ -231,7 +231,7 @@ "sign_out_modal_text": "Ваша бібліотека пов'язана з поточним обліковим записом. При виході з системи ваша бібліотека буде недоступною, і прогрес не буде збережено. Продовжити вихід?", "sign_out_modal_title": "Ви впевнені?", "successfully_signed_out": "Успішний вихід з акаунту", - "total_play_time": "Всього зіграно: {{amount}}", + "total_play_time": "Всього зіграно", "try_again": "Будь ласка, попробуйте ще раз" } } diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 9bab7516..664877fa 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -290,7 +290,7 @@ "last_time_played": "上次游玩时间 {{period}}", "activity": "近期活动", "library": "库", - "total_play_time": "总游戏时长: {{amount}}", + "total_play_time": "总游戏时长", "no_recent_activity_title": "Emmm… 这里暂时啥都没有", "no_recent_activity_description": "你最近没玩过任何游戏。是时候做出改变了!", "display_name": "昵称", @@ -359,11 +359,11 @@ "achievement_unlocked": "成就已解锁", "user_achievements": "{{displayName}}的成就", "your_achievements": "你的成就", - "unlocked_at": "解锁于:", + "unlocked_at": "解锁于: {{date}}", "subscription_needed": "需要订阅 Hydra Cloud 才能看到此内容", "new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就" }, - "tour": { + "hydra_cloud": { "subscription_tour_title": "Hydra 云订阅", "subscribe_now": "现在订购", "cloud_saving": "云存档", diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts index 0c117140..0b665212 100644 --- a/src/main/events/user/get-compared-unlocked-achievements.ts +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -13,6 +13,9 @@ const getComparedUnlockedAchievements = async ( where: { id: 1 }, }); + const showHiddenAchievementsDescription = + userPreferences?.showHiddenAchievementsDescription || false; + return HydraApi.get( `/users/${userId}/games/achievements/compare`, { @@ -21,15 +24,35 @@ const getComparedUnlockedAchievements = async ( language: userPreferences?.language || "en", } ).then((achievements) => { - const sortedAchievements = achievements.achievements.sort((a, b) => { - if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1; - if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1; - if (a.targetStat.unlocked && b.targetStat.unlocked) { - return b.targetStat.unlockTime! - a.targetStat.unlockTime!; - } + const sortedAchievements = achievements.achievements + .sort((a, b) => { + if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1; + if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1; + if (a.targetStat.unlocked && b.targetStat.unlocked) { + return b.targetStat.unlockTime! - a.targetStat.unlockTime!; + } - return Number(a.hidden) - Number(b.hidden); - }); + return Number(a.hidden) - Number(b.hidden); + }) + .map((achievement) => { + if (!achievement.hidden) return achievement; + + if (!achievement.ownerStat) { + return { + ...achievement, + description: "", + }; + } + + if (!showHiddenAchievementsDescription && achievement.hidden) { + return { + ...achievement, + description: "", + }; + } + + return achievement; + }); return { ...achievements, diff --git a/src/main/events/user/get-user.ts b/src/main/events/user/get-user.ts index 6bbab9c4..f51b0456 100644 --- a/src/main/events/user/get-user.ts +++ b/src/main/events/user/get-user.ts @@ -11,7 +11,7 @@ const getSteamGame = async (objectId: string) => { }); return { - title: steamGame.name, + title: steamGame.name as string, iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon), }; } catch (err) { @@ -67,8 +67,25 @@ const getUser = async ( } } + const friends = await Promise.all( + profile.friends.map(async (friend) => { + if (!friend.currentGame) return friend; + + const currentGame = await getSteamGame(friend.currentGame.objectId); + + return { + ...friend, + currentGame: { + ...friend.currentGame, + ...currentGame, + }, + }; + }) + ); + return { ...profile, + friends, libraryGames, recentGames, }; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 50780e95..dd8c877d 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -7,8 +7,9 @@ import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; import { Game } from "@main/entity"; -import { achievementsLogger } from "../logger"; import { publishNewAchievementNotification } from "../notifications"; +import { SubscriptionRequiredError } from "@shared"; +import { achievementsLogger } from "../logger"; const saveAchievementsOnLocal = async ( objectId: string, @@ -120,10 +121,14 @@ export const mergeAchievements = async ( } if (game.remoteId) { - await HydraApi.put("/profile/games/achievements", { - id: game.remoteId, - achievements: mergedLocalAchievements, - }) + await HydraApi.put( + "/profile/games/achievements", + { + id: game.remoteId, + achievements: mergedLocalAchievements, + }, + { needsSubscription: !newAchievements.length } + ) .then((response) => { return saveAchievementsOnLocal( response.objectId, @@ -133,7 +138,13 @@ export const mergeAchievements = async ( ); }) .catch((err) => { - achievementsLogger.error(err); + if (err! instanceof SubscriptionRequiredError) { + achievementsLogger.log( + "Achievements not synchronized on API due to lack of subscription", + game.objectID, + game.title + ); + } return saveAchievementsOnLocal( game.objectID, diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index bac1486a..63dd9b16 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -23,7 +23,7 @@ interface HydraApiUserAuth { authToken: string; refreshToken: string; expirationTimestamp: number; - subscription: { expiresAt: Date | null } | null; + subscription: { expiresAt: Date | string | null } | null; } export class HydraApi { @@ -159,7 +159,11 @@ export class HydraApi { config.method, config.baseURL, config.url, - omit(config.headers, ["accessToken", "refreshToken"]), + omit(config.headers, [ + "accessToken", + "refreshToken", + "Authorization", + ]), Array.isArray(data) ? data : omit(data, ["accessToken", "refreshToken"]) @@ -182,8 +186,6 @@ export class HydraApi { ); } - await getUserData(); - const userAuth = await userAuthRepository.findOne({ where: { id: 1 }, relations: { subscription: true }, @@ -197,6 +199,14 @@ export class HydraApi { ? { expiresAt: userAuth.subscription?.expiresAt } : null, }; + + const updatedUserData = await getUserData(); + + this.userAuth.subscription = updatedUserData?.subscription + ? { + expiresAt: updatedUserData.subscription.expiresAt, + } + : null; } private static sendSignOutEvent() { @@ -284,10 +294,8 @@ export class HydraApi { await this.revalidateAccessTokenIfExpired(); } - if (needsSubscription) { - if (!(await this.hasActiveSubscription())) { - throw new SubscriptionRequiredError(); - } + if (needsSubscription && !this.hasActiveSubscription()) { + throw new SubscriptionRequiredError(); } } diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index 1b8cb1e0..7e924454 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -42,6 +42,7 @@ export const getUserData = () => { }) .catch(async (err) => { if (err instanceof UserNotLoggedInError) { + logger.info("User is not logged in", err); return null; } logger.error("Failed to get logged user"); diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 9c1c29f5..5fefe90c 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -27,6 +27,8 @@ import { useTranslation } from "react-i18next"; import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { downloadSourcesWorker } from "./workers"; import { downloadSourcesTable } from "./dexie"; +import { useSubscription } from "./hooks/use-subscription"; +import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal"; export interface AppProps { children: React.ReactNode; @@ -43,21 +45,21 @@ export function App() { const { clearDownload, setLastPacket } = useDownload(); const { + userDetails, + hasActiveSubscription, isFriendsModalVisible, friendRequetsModalTab, friendModalUserId, syncFriendRequests, hideFriendsModal, - } = useUserDetails(); - - const { - userDetails, - hasActiveSubscription, fetchUserDetails, updateUserDetails, clearUserDetails, } = useUserDetails(); + const { hideHydraCloudModal, isHydraCloudModalVisible, hydraCloudFeature } = + useSubscription(); + const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -255,6 +257,12 @@ export function App() { onClose={handleToastClose} /> + + {userDetails && ( + + + + + + + + + + + + diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index b28b9e6d..09ac7257 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -51,7 +51,6 @@ export const gameDetailsContext = createContext({ setShowGameOptionsModal: () => {}, setShowRepacksModal: () => {}, setHasNSFWContentBlocked: () => {}, - handleClickOpenCheckout: () => {}, }); const { Provider } = gameDetailsContext; @@ -100,11 +99,6 @@ export function GameDetailsContextProvider({ (state) => state.userPreferences.value ); - const handleClickOpenCheckout = () => { - // TODO: show modal before redirecting to checkout page - window.electron.openCheckout(); - }; - const updateGame = useCallback(async () => { return window.electron .getGameByObjectId(objectId!) @@ -279,7 +273,6 @@ export function GameDetailsContextProvider({ updateGame, setShowRepacksModal, setShowGameOptionsModal, - handleClickOpenCheckout, }} > {children} diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index 842065cc..49718430 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -29,5 +29,4 @@ export interface GameDetailsContext { setShowRepacksModal: React.Dispatch>; setShowGameOptionsModal: React.Dispatch>; setHasNSFWContentBlocked: React.Dispatch>; - handleClickOpenCheckout: () => void; } diff --git a/src/renderer/src/features/index.ts b/src/renderer/src/features/index.ts index e3c1c7a6..c0b8753d 100644 --- a/src/renderer/src/features/index.ts +++ b/src/renderer/src/features/index.ts @@ -5,5 +5,6 @@ export * from "./window-slice"; export * from "./toast-slice"; export * from "./user-details-slice"; export * from "./running-game-slice"; +export * from "./subscription-slice"; export * from "./repacks-slice"; export * from "./catalogue-search"; diff --git a/src/renderer/src/features/subscription-slice.ts b/src/renderer/src/features/subscription-slice.ts new file mode 100644 index 00000000..d192ef6f --- /dev/null +++ b/src/renderer/src/features/subscription-slice.ts @@ -0,0 +1,32 @@ +import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; +import type { HydraCloudFeature } from "@types"; + +export interface SubscriptionState { + isHydraCloudModalVisible: boolean; + feature: HydraCloudFeature | ""; +} + +const initialState: SubscriptionState = { + isHydraCloudModalVisible: false, + feature: "", +}; + +export const subscriptionSlice = createSlice({ + name: "subscription", + initialState, + reducers: { + setHydraCloudModalVisible: ( + state, + action: PayloadAction + ) => { + state.isHydraCloudModalVisible = true; + state.feature = action.payload; + }, + setHydraCloudModalHidden: (state) => { + state.isHydraCloudModalVisible = false; + }, + }, +}); + +export const { setHydraCloudModalVisible, setHydraCloudModalHidden } = + subscriptionSlice.actions; diff --git a/src/renderer/src/hooks/use-subscription.ts b/src/renderer/src/hooks/use-subscription.ts new file mode 100644 index 00000000..273eb0ef --- /dev/null +++ b/src/renderer/src/hooks/use-subscription.ts @@ -0,0 +1,33 @@ +import { useCallback } from "react"; +import { useAppDispatch, useAppSelector } from "./redux"; +import { + setHydraCloudModalVisible, + setHydraCloudModalHidden, +} from "@renderer/features"; +import { HydraCloudFeature } from "@types"; + +export function useSubscription() { + const dispatch = useAppDispatch(); + + const { isHydraCloudModalVisible, feature } = useAppSelector( + (state) => state.subscription + ); + + const showHydraCloudModal = useCallback( + (feature: HydraCloudFeature) => { + dispatch(setHydraCloudModalVisible(feature)); + }, + [dispatch] + ); + + const hideHydraCloudModal = useCallback(() => { + dispatch(setHydraCloudModalHidden()); + }, [dispatch]); + + return { + isHydraCloudModalVisible, + hydraCloudFeature: feature, + showHydraCloudModal, + hideHydraCloudModal, + }; +} diff --git a/src/renderer/src/pages/achievements/achievement-list.tsx b/src/renderer/src/pages/achievements/achievement-list.tsx new file mode 100644 index 00000000..6e6944de --- /dev/null +++ b/src/renderer/src/pages/achievements/achievement-list.tsx @@ -0,0 +1,90 @@ +import { useDate } from "@renderer/hooks"; +import type { UserAchievement } from "@types"; +import { useTranslation } from "react-i18next"; +import * as styles from "./achievements.css"; +import { EyeClosedIcon } from "@primer/octicons-react"; +import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; +import { useSubscription } from "@renderer/hooks/use-subscription"; +import { vars } from "@renderer/theme.css"; + +interface AchievementListProps { + achievements: UserAchievement[]; +} + +export function AchievementList({ achievements }: AchievementListProps) { + const { t } = useTranslation("achievement"); + const { showHydraCloudModal } = useSubscription(); + const { formatDateTime } = useDate(); + + return ( +
    + {achievements.map((achievement, index) => ( +
  • + {achievement.displayName} + +
    +

    + {achievement.hidden && ( + + + + )} + {achievement.displayName} +

    +

    {achievement.description}

    +
    +
    + {achievement.points != undefined ? ( +
    + +

    {achievement.points}

    +
    + ) : ( + + )} + {achievement.unlockTime && ( +
    + {formatDateTime(achievement.unlockTime)} +
    + )} +
    +
  • + ))} +
+ ); +} diff --git a/src/renderer/src/pages/achievements/achievement-panel.css.ts b/src/renderer/src/pages/achievements/achievement-panel.css.ts new file mode 100644 index 00000000..f8daeab9 --- /dev/null +++ b/src/renderer/src/pages/achievements/achievement-panel.css.ts @@ -0,0 +1,71 @@ +import { style } from "@vanilla-extract/css"; +import { recipe } from "@vanilla-extract/recipes"; + +import { SPACING_UNIT, vars } from "../../theme.css"; + +export const panel = style({ + width: "100%", + padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`, + backgroundColor: vars.color.background, + display: "flex", + flexDirection: "column", + alignItems: "start", + justifyContent: "space-between", + borderBottom: `solid 1px ${vars.color.border}`, +}); + +export const content = style({ + display: "flex", + gap: `${SPACING_UNIT}px`, + justifyContent: "center", +}); + +export const actions = style({ + display: "flex", + gap: `${SPACING_UNIT}px`, +}); + +export const downloadDetailsRow = style({ + gap: `${SPACING_UNIT}px`, + display: "flex", + color: vars.color.body, + alignItems: "center", +}); + +export const downloadsLink = style({ + color: vars.color.body, + textDecoration: "underline", +}); + +export const progressBar = recipe({ + base: { + position: "absolute", + bottom: "0", + left: "0", + width: "100%", + height: "3px", + transition: "all ease 0.2s", + "::-webkit-progress-bar": { + backgroundColor: "transparent", + }, + "::-webkit-progress-value": { + backgroundColor: vars.color.muted, + }, + }, + variants: { + disabled: { + true: { + opacity: vars.opacity.disabled, + }, + }, + }, +}); + +export const link = style({ + textAlign: "start", + color: vars.color.body, + ":hover": { + textDecoration: "underline", + cursor: "pointer", + }, +}); diff --git a/src/renderer/src/pages/achievements/achievement-panel.tsx b/src/renderer/src/pages/achievements/achievement-panel.tsx new file mode 100644 index 00000000..bda25a89 --- /dev/null +++ b/src/renderer/src/pages/achievements/achievement-panel.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from "react-i18next"; +import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; +import { UserAchievement } from "@types"; +import { useSubscription } from "@renderer/hooks/use-subscription"; +import { useUserDetails } from "@renderer/hooks"; +import { vars } from "@renderer/theme.css"; +import * as styles from "./achievement-panel.css"; + +export interface AchievementPanelProps { + achievements: UserAchievement[]; +} + +export function AchievementPanel({ achievements }: AchievementPanelProps) { + const { t } = useTranslation("achievement"); + const { hasActiveSubscription } = useUserDetails(); + const { showHydraCloudModal } = useSubscription(); + + const achievementsPointsTotal = achievements.reduce( + (acc, achievement) => acc + (achievement.points ?? 0), + 0 + ); + + const achievementsPointsEarnedSum = achievements.reduce( + (acc, achievement) => + acc + (achievement.unlocked ? (achievement.points ?? 0) : 0), + 0 + ); + + if (!hasActiveSubscription) { + return ( +
+
+ {t("earned_points")} + ??? / ??? +
+ +
+ ); + } + + return ( +
+
+ {t("earned_points")} + {achievementsPointsEarnedSum} / {achievementsPointsTotal} +
+
+ ); +} diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index a7a9aaa8..e7d47f34 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -1,9 +1,8 @@ import { setHeaderTitle } from "@renderer/features"; -import { useAppDispatch, useDate, useUserDetails } from "@renderer/hooks"; +import { useAppDispatch, useUserDetails } from "@renderer/hooks"; import { steamUrlBuilder } from "@shared"; import { useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import * as styles from "./achievements.css"; import { buildGameDetailsPath, formatDownloadProgress, @@ -11,11 +10,16 @@ import { import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; -import type { ComparedAchievements, UserAchievement } from "@types"; +import type { ComparedAchievements } from "@types"; import { average } from "color.js"; import Color from "color"; import { Link } from "@renderer/components"; import { ComparedAchievementList } from "./compared-achievement-list"; +import * as styles from "./achievements.css"; +import { AchievementList } from "./achievement-list"; +import { AchievementPanel } from "./achievement-panel"; +import { ComparedAchievementPanel } from "./compared-achievement-panel"; +import { useSubscription } from "@renderer/hooks/use-subscription"; interface UserInfo { id: string; @@ -30,10 +34,6 @@ interface AchievementsContentProps { comparedAchievements: ComparedAchievements | null; } -interface AchievementListProps { - achievements: UserAchievement[]; -} - interface AchievementSummaryProps { user: UserInfo; isComparison?: boolean; @@ -42,7 +42,7 @@ interface AchievementSummaryProps { function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { const { t } = useTranslation("achievement"); const { userDetails, hasActiveSubscription } = useUserDetails(); - const { handleClickOpenCheckout } = useContext(gameDetailsContext); + const { showHydraCloudModal } = useSubscription(); const getProfileImage = ( user: Pick @@ -93,7 +93,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {

@@ -171,38 +171,6 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { ); } -function AchievementList({ achievements }: AchievementListProps) { - const { t } = useTranslation("achievement"); - const { formatDateTime } = useDate(); - - return ( -
    - {achievements.map((achievement, index) => ( -
  • - {achievement.displayName} -
    -

    {achievement.displayName}

    -

    {achievement.description}

    -
    - {achievement.unlockTime && ( -
    - {t("unlocked_at")} -

    {formatDateTime(achievement.unlockTime)}

    -
    - )} -
  • - ))} -
- ); -} - export function AchievementsContent({ otherUser, comparedAchievements, @@ -355,9 +323,15 @@ export function AchievementsContent({ )} {otherUser ? ( - + <> + + + ) : ( - + <> + + + )} diff --git a/src/renderer/src/pages/achievements/compared-achievement-list.tsx b/src/renderer/src/pages/achievements/compared-achievement-list.tsx index 21f11936..44aec686 100644 --- a/src/renderer/src/pages/achievements/compared-achievement-list.tsx +++ b/src/renderer/src/pages/achievements/compared-achievement-list.tsx @@ -1,8 +1,13 @@ import type { ComparedAchievements } from "@types"; import * as styles from "./achievements.css"; -import { CheckCircleIcon, LockIcon } from "@primer/octicons-react"; +import { + CheckCircleIcon, + EyeClosedIcon, + LockIcon, +} from "@primer/octicons-react"; import { useDate } from "@renderer/hooks"; import { SPACING_UNIT } from "@renderer/theme.css"; +import { useTranslation } from "react-i18next"; export interface ComparedAchievementListProps { achievements: ComparedAchievements; @@ -11,6 +16,7 @@ export interface ComparedAchievementListProps { export function ComparedAchievementList({ achievements, }: ComparedAchievementListProps) { + const { t } = useTranslation("achievement"); const { formatDateTime } = useDate(); return ( @@ -43,7 +49,17 @@ export function ComparedAchievementList({ loading="lazy" />
-

{achievement.displayName}

+

+ {achievement.hidden && ( + + + + )} + {achievement.displayName} +

{achievement.description}

@@ -58,11 +74,9 @@ export function ComparedAchievementList({ gap: `${SPACING_UNIT}px`, justifyContent: "center", }} + title={formatDateTime(achievement.ownerStat.unlockTime!)} > - - {formatDateTime(achievement.ownerStat.unlockTime!)} - ) : (
- - {formatDateTime(achievement.targetStat.unlockTime!)} -
) : (
+
+ {t("available_points")} {" "} + {achievements.achievementsPointsTotal} +
+ {hasActiveSubscription && ( +
+ + {achievements.owner.achievementsPointsEarnedSum ?? 0} +
+ )} +
+ + {achievements.target.achievementsPointsEarnedSum} +
+
+ ); +} 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 70ce165f..12495231 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -14,6 +14,7 @@ import { steamUrlBuilder } from "@shared"; import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif"; import { useUserDetails } from "@renderer/hooks"; +import { useSubscription } from "@renderer/hooks/use-subscription"; const HERO_ANIMATION_THRESHOLD = 25; @@ -31,9 +32,10 @@ export function GameDetailsContent() { gameColor, setGameColor, hasNSFWContentBlocked, - handleClickOpenCheckout, } = useContext(gameDetailsContext); + const { showHydraCloudModal } = useSubscription(); + const { userDetails, hasActiveSubscription } = useUserDetails(); const { setShowCloudSyncModal, getGameArtifacts } = @@ -104,7 +106,7 @@ export function GameDetailsContent() { } if (!hasActiveSubscription) { - handleClickOpenCheckout(); + showHydraCloudModal("backup"); return; } diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 35982b19..191d9ac1 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -8,7 +8,7 @@ import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; import { Downloader, formatBytes, getDownloadersForUris } from "@shared"; import type { GameRepack } from "@types"; -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT } from "@renderer/theme.css"; import { DOWNLOADER_NAME } from "@renderer/constants"; import { useAppSelector, useToast } from "@renderer/hooks"; @@ -159,16 +159,6 @@ export function DownloadSettingsModal({ ))} - - {selectedDownloader != null && - selectedDownloader !== Downloader.Torrent && ( -

- - {t("warning")} - {" "} - {t("hydra_needs_to_remain_open")} -

- )}
("minimum"); - const { - gameTitle, - shopDetails, - objectId, - shop, - stats, - achievements, - handleClickOpenCheckout, - } = useContext(gameDetailsContext); + const { gameTitle, shopDetails, objectId, shop, stats, achievements } = + useContext(gameDetailsContext); + + const { showHydraCloudModal } = useSubscription(); const { t } = useTranslation("game_details"); const { formatDateTime } = useDate(); @@ -179,7 +175,7 @@ export function Sidebar() { {!hasActiveSubscription && ( + )} + + )} + + {(isMe || userStats.achievementsPointsEarnedSum !== undefined) && ( +
  • +

    {t("earned_points")}

    + {userStats.achievementsPointsEarnedSum !== undefined ? ( +
    +

    + + {numberFormatter.format( + userStats.achievementsPointsEarnedSum.value + )} +

    +

    + {t("top_percentile", { + percentile: + userStats.achievementsPointsEarnedSum.topPercentile, + })} +

    +
    + ) : ( + + )} +
  • + )} + +
  • +

    {t("total_play_time")}

    +
    +

    + + {formatPlayTime(userStats.totalPlayTimeInSeconds.value)} +

    +

    + {t("top_percentile", { + percentile: userStats.totalPlayTimeInSeconds.topPercentile, + })} +

    +
    +
  • + +
    + + ); +} diff --git a/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.css.ts b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.css.ts new file mode 100644 index 00000000..a164c900 --- /dev/null +++ b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.css.ts @@ -0,0 +1,79 @@ +import { SPACING_UNIT, vars } from "../../../theme.css"; +import { style } from "@vanilla-extract/css"; + +export const friendListDisplayName = style({ + fontWeight: "bold", + fontSize: vars.size.body, + textAlign: "left", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +}); + +export const friendListContainer = style({ + display: "flex", + gap: `${SPACING_UNIT * 3}px`, + alignItems: "center", + borderRadius: "4px", + border: `solid 1px ${vars.color.border}`, + width: "100%", + height: "54px", + minHeight: "54px", + transition: "all ease 0.2s", + position: "relative", + ":hover": { + backgroundColor: "rgba(255, 255, 255, 0.15)", + }, +}); + +export const friendListButton = style({ + display: "flex", + alignItems: "center", + position: "absolute", + cursor: "pointer", + height: "100%", + width: "100%", + flexDirection: "row", + color: vars.color.body, + gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`, + padding: `0 ${SPACING_UNIT}px`, +}); + +export const friendRequestItem = style({ + color: vars.color.body, + ":hover": { + backgroundColor: "rgba(255, 255, 255, 0.15)", + }, +}); + +export const acceptRequestButton = style({ + cursor: "pointer", + color: vars.color.body, + width: "28px", + height: "28px", + ":hover": { + color: vars.color.success, + }, +}); + +export const cancelRequestButton = style({ + cursor: "pointer", + color: vars.color.body, + width: "28px", + height: "28px", + ":hover": { + color: vars.color.danger, + }, +}); + +export const friendCodeButton = style({ + color: vars.color.body, + cursor: "pointer", + display: "flex", + gap: `${SPACING_UNIT / 2}px`, + alignItems: "center", + transition: "all ease 0.2s", + ":hover": { + color: vars.color.muted, + }, +}); diff --git a/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.tsx b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.tsx new file mode 100644 index 00000000..fd44ce30 --- /dev/null +++ b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.tsx @@ -0,0 +1,38 @@ +import { Button, Modal } from "@renderer/components"; +import { SPACING_UNIT } from "@renderer/theme.css"; +import { useTranslation } from "react-i18next"; + +export interface HydraCloudModalProps { + feature: string; + visible: boolean; + onClose: () => void; +} + +export const HydraCloudModal = ({ + feature, + visible, + onClose, +}: HydraCloudModalProps) => { + const { t } = useTranslation("hydra_cloud"); + + const handleClickOpenCheckout = () => { + window.electron.openCheckout(); + }; + + return ( + +
    + {t("hydra_cloud_feature_found")} + +
    +
    + ); +}; diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index 36b60309..e771f98e 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -7,6 +7,7 @@ import { toastSlice, userDetailsSlice, gameRunningSlice, + subscriptionSlice, repacksSlice, catalogueSearchSlice, } from "@renderer/features"; @@ -20,6 +21,7 @@ export const store = configureStore({ toast: toastSlice.reducer, userDetails: userDetailsSlice.reducer, gameRunning: gameRunningSlice.reducer, + subscription: subscriptionSlice.reducer, repacks: repacksSlice.reducer, catalogueSearch: catalogueSearchSlice.reducer, }, diff --git a/src/types/index.ts b/src/types/index.ts index def96fb9..e6ca334b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,6 +14,11 @@ export type GameShop = "steam" | "epic"; export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL"; +export type HydraCloudFeature = + | "achievements" + | "backup" + | "achievements-points"; + export interface GameRepack { id: number; title: string; @@ -33,12 +38,14 @@ export interface AchievementData { icon: string; icongray: string; hidden: boolean; + points?: number; } export interface UserAchievement { name: string; hidden: boolean; displayName: string; + points?: number; description?: string; unlocked: boolean; unlockTime: number | null; @@ -85,6 +92,7 @@ export interface UserGame { lastTimePlayed: Date | null; unlockedAchievementCount: number; achievementCount: number; + achievementsPointsEarnedSum: number; } export interface DownloadQueue { @@ -194,6 +202,13 @@ export interface UserFriend { profileImageUrl: string | null; createdAt: string; updatedAt: string; + currentGame: { + title: string; + iconUrl: string; + objectId: string; + shop: GameShop; + sessionDurationInSeconds: number; + } | null; } export interface UserFriends { @@ -324,9 +339,17 @@ export interface TrendingGame { logo: string | null; } +export interface UserStatsPercentile { + value: number; + topPercentile: number; +} + export interface UserStats { libraryCount: number; friendsCount: number; + totalPlayTimeInSeconds: UserStatsPercentile; + achievementsPointsEarnedSum?: UserStatsPercentile; + unlockedAchievementSum?: number; } export interface UnlockedAchievement { @@ -354,15 +377,18 @@ export interface GameArtifact { } export interface ComparedAchievements { + achievementsPointsTotal: number; owner: { totalAchievementCount: number; unlockedAchievementCount: number; + achievementsPointsEarnedSum?: number; }; target: { displayName: string; profileImageUrl: string; totalAchievementCount: number; unlockedAchievementCount: number; + achievementsPointsEarnedSum: number; }; achievements: { hidden: boolean;