mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 05:24:55 +03:00
Merge branch 'feat/new-catalogue' of https://github.com/hydralauncher/hydra into feat/new-catalogue
This commit is contained in:
commit
85422837b0
@ -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": "الحفظ السحابي",
|
||||
|
@ -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": "Запазване в облака",
|
||||
|
@ -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ó",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "Көрсету аты",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -231,7 +231,7 @@
|
||||
"sign_out_modal_text": "Ваша бібліотека пов'язана з поточним обліковим записом. При виході з системи ваша бібліотека буде недоступною, і прогрес не буде збережено. Продовжити вихід?",
|
||||
"sign_out_modal_title": "Ви впевнені?",
|
||||
"successfully_signed_out": "Успішний вихід з акаунту",
|
||||
"total_play_time": "Всього зіграно: {{amount}}",
|
||||
"total_play_time": "Всього зіграно",
|
||||
"try_again": "Будь ласка, попробуйте ще раз"
|
||||
}
|
||||
}
|
||||
|
@ -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": "云存档",
|
||||
|
@ -13,6 +13,9 @@ const getComparedUnlockedAchievements = async (
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const showHiddenAchievementsDescription =
|
||||
userPreferences?.showHiddenAchievementsDescription || false;
|
||||
|
||||
return HydraApi.get<ComparedAchievements>(
|
||||
`/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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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}
|
||||
/>
|
||||
|
||||
<HydraCloudModal
|
||||
visible={isHydraCloudModalVisible}
|
||||
onClose={hideHydraCloudModal}
|
||||
feature={hydraCloudFeature}
|
||||
/>
|
||||
|
||||
{userDetails && (
|
||||
<UserFriendModal
|
||||
visible={isFriendsModalVisible}
|
||||
|
13
src/renderer/src/assets/icons/hydra.svg
Normal file
13
src/renderer/src/assets/icons/hydra.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="55" height="49" viewBox="0 0 55 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.8501 29.1176L19.9196 28.3235L20.6957 25.6764L20.437 24.8823L18.1088 25.9411L14.487 24.3528L10.0891 23.0293L5.69128 23.5587L3.10431 25.6764L2.58691 29.1176L4.1391 33.6177L5.69128 36.7942L8.53695 38.9118L10.8652 38.3824L13.9696 34.9412V31.5L12.9348 29.1176V32.2941L10.8652 34.9412H7.50216L4.91519 32.2941L5.69128 28.3235L9.57174 26.4705L13.9696 27.5294L17.8501 29.1176Z" fill="white"/>
|
||||
<path d="M36.9585 29.1176L34.889 28.3235L34.1129 25.6764L34.3716 24.8823L36.6998 25.9411L40.3216 24.3528L44.7195 23.0293L49.1173 23.5587L51.7043 25.6764L52.2217 29.1176L50.6695 33.6177L49.1173 36.7942L46.2716 38.9118L43.9434 38.3824L40.839 34.9412V31.5L41.8738 29.1176V32.2941L43.9434 34.9412H47.3064L49.8934 32.2941L49.1173 28.3235L45.2369 26.4705L40.839 27.5294L36.9585 29.1176Z" fill="white"/>
|
||||
<path d="M40.3873 19.4005L38.8784 19.9071L38.7685 19.0593L38.5553 17.7049L39.811 14.777L41.6564 11.5721L44.483 9.44001L47.1023 9.23162L49.2244 10.9371L50.7089 14.4032L51.4925 17.1031L50.9665 19.9071L49.3381 20.8916L45.7182 20.6206L43.8957 18.6282L43.2332 16.675L44.9154 18.5142L47.5156 18.8992L49.4627 17.0344L49.5586 14.0673L47.0064 12.1987L43.7784 13.2776L41.7929 16.3293L40.3873 19.4005Z" fill="white"/>
|
||||
<path d="M14.0238 19.4005L15.5327 19.9071L15.6426 19.0593L15.8559 17.7049L14.6001 14.777L12.7548 11.5721L9.92812 9.44001L7.30879 9.23162L5.18676 10.9371L3.70221 14.4032L2.91861 17.1031L3.44468 19.9071L5.07308 20.8916L8.69292 20.6206L10.5154 18.6282L11.178 16.675L9.4957 18.5142L6.89555 18.8992L4.94841 17.0344L4.8525 14.0673L7.4047 12.1987L10.6327 13.2776L12.6182 16.3293L14.0238 19.4005Z" fill="white"/>
|
||||
<path d="M19.9494 36.4343L22.554 34.3372L21.9876 34.0884L20.9528 31.9707L19.9494 32.5001L17.5898 34.0884L15.3904 37.377L14.744 40.4414L15.2615 43.0885L17.0724 45.4709L20.9528 47.3238L23.6932 46.5297L25.435 44.7653L26.0028 42.9192L25.6093 39.0508L25.0919 37.7913L23.6932 37.0002L24.3158 39.105L24.3158 42.0296L22.3597 43.9181L19.9494 43.9181L18.0225 42.0296L17.4949 39.105L19.9494 36.4343Z" fill="white"/>
|
||||
<path d="M35.0955 36.4343L32.4909 34.3372L33.0573 34.0884L34.0921 31.9707L35.0955 32.5001L37.4552 34.0884L39.6545 37.377L40.3009 40.4414L39.7834 43.0885L37.9725 45.4709L34.0921 47.3238L31.3518 46.5297L29.6099 44.7653L29.0421 42.9192L29.4356 39.0508L29.953 37.7913L31.3518 37.0002L30.7291 39.105L30.7291 42.0296L32.6852 43.9181L35.0955 43.9181L37.0224 42.0296L37.55 39.105L35.0955 36.4343Z" fill="white"/>
|
||||
<path d="M18.8447 8.70593V5.79413L20.1382 5H22.9839L27.3817 5.79413L31.7796 5H34.6252L35.9187 5.79413V8.70593L38.7644 11.353L37.2122 15.0589L37.9883 20.8825L35.9187 23.0002L33.3317 23.7943L32.8144 26.1767L33.3317 28.8238L32.8144 30.9415L31.7796 33.0591L30.2274 33.8533H27.3817H24.536L22.9839 33.0591L21.9491 30.9415L21.4317 28.8238L21.9491 26.1767L21.4317 23.7943L18.8447 23.0002L16.7751 20.8825L17.5512 15.0589L15.999 11.353L18.8447 8.70593Z" fill="white"/>
|
||||
<path d="M15.5205 6.88232L16.2966 8.73528L17.5901 7.67645V4.76465L15.5205 6.88232Z" fill="white"/>
|
||||
<path d="M39.2861 6.88232L38.51 8.73528L37.2166 7.67645V4.76465L39.2861 6.88232Z" fill="white"/>
|
||||
<path d="M18.3667 2.11767L18.6254 4.23534L19.4015 3.70593H20.9537L20.4363 2.11767L17.0732 0L18.3667 2.11767Z" fill="white"/>
|
||||
<path d="M35.6997 2.11767L35.441 4.23534L34.6649 3.70593H33.1127L33.6301 2.11767L36.9932 0L35.6997 2.11767Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
@ -67,10 +67,10 @@ export function Header() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!location.pathname.startsWith("/catalogue")) {
|
||||
if (!location.pathname.startsWith("/catalogue") && searchValue) {
|
||||
dispatch(setFilters({ title: "" }));
|
||||
}
|
||||
}, [location.pathname, dispatch]);
|
||||
}, [location.pathname, searchValue, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -51,7 +51,6 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
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}
|
||||
|
@ -29,5 +29,4 @@ export interface GameDetailsContext {
|
||||
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setShowGameOptionsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setHasNSFWContentBlocked: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleClickOpenCheckout: () => void;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import type { CatalogueSearchPayload } from "@types";
|
||||
|
||||
export interface CatalogueSearchState {
|
||||
filters: CatalogueSearchPayload;
|
||||
page: number;
|
||||
}
|
||||
|
||||
const initialState: CatalogueSearchState = {
|
||||
@ -16,6 +17,7 @@ const initialState: CatalogueSearchState = {
|
||||
genres: [],
|
||||
developers: [],
|
||||
},
|
||||
page: 1,
|
||||
};
|
||||
|
||||
export const catalogueSearchSlice = createSlice({
|
||||
@ -27,11 +29,20 @@ export const catalogueSearchSlice = createSlice({
|
||||
action: PayloadAction<Partial<CatalogueSearchPayload>>
|
||||
) => {
|
||||
state.filters = { ...state.filters, ...action.payload };
|
||||
state.page = initialState.page;
|
||||
},
|
||||
clearFilters: (state) => {
|
||||
state.filters = initialState.filters;
|
||||
state.page = initialState.page;
|
||||
},
|
||||
setPage: (state, action: PayloadAction<number>) => {
|
||||
state.page = action.payload;
|
||||
},
|
||||
clearPage: (state) => {
|
||||
state.page = initialState.page;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setFilters, clearFilters } = catalogueSearchSlice.actions;
|
||||
export const { setFilters, clearFilters, setPage, clearPage } =
|
||||
catalogueSearchSlice.actions;
|
||||
|
@ -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";
|
||||
|
32
src/renderer/src/features/subscription-slice.ts
Normal file
32
src/renderer/src/features/subscription-slice.ts
Normal file
@ -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<HydraCloudFeature>
|
||||
) => {
|
||||
state.isHydraCloudModalVisible = true;
|
||||
state.feature = action.payload;
|
||||
},
|
||||
setHydraCloudModalHidden: (state) => {
|
||||
state.isHydraCloudModalVisible = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setHydraCloudModalVisible, setHydraCloudModalHidden } =
|
||||
subscriptionSlice.actions;
|
33
src/renderer/src/hooks/use-subscription.ts
Normal file
33
src/renderer/src/hooks/use-subscription.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
90
src/renderer/src/pages/achievements/achievement-list.tsx
Normal file
90
src/renderer/src/pages/achievements/achievement-list.tsx
Normal file
@ -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 (
|
||||
<ul className={styles.list}>
|
||||
{achievements.map((achievement, index) => (
|
||||
<li key={index} className={styles.listItem} style={{ display: "flex" }}>
|
||||
<img
|
||||
className={styles.listItemImage({
|
||||
unlocked: achievement.unlocked,
|
||||
})}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
{achievement.hidden && (
|
||||
<span
|
||||
style={{ display: "flex" }}
|
||||
title={t("hidden_achievement_tooltip")}
|
||||
>
|
||||
<EyeClosedIcon size={12} />
|
||||
</span>
|
||||
)}
|
||||
{achievement.displayName}
|
||||
</h4>
|
||||
<p>{achievement.description}</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
{achievement.points != undefined ? (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: "4px" }}
|
||||
title={t("achievement_earn_points", {
|
||||
points: achievement.points,
|
||||
})}
|
||||
>
|
||||
<HydraIcon width={20} height={20} />
|
||||
<p style={{ fontSize: "1.1em" }}>{achievement.points}</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
cursor: "pointer",
|
||||
color: vars.color.warning,
|
||||
}}
|
||||
title={t("achievement_earn_points", {
|
||||
points: "???",
|
||||
})}
|
||||
>
|
||||
<HydraIcon width={20} height={20} />
|
||||
<p style={{ fontSize: "1.1em" }}>???</p>
|
||||
</button>
|
||||
)}
|
||||
{achievement.unlockTime && (
|
||||
<div
|
||||
title={t("unlocked_at", {
|
||||
date: formatDateTime(achievement.unlockTime),
|
||||
})}
|
||||
style={{ whiteSpace: "nowrap", gap: "4px", display: "flex" }}
|
||||
>
|
||||
<small>{formatDateTime(achievement.unlockTime)}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
71
src/renderer/src/pages/achievements/achievement-panel.css.ts
Normal file
71
src/renderer/src/pages/achievements/achievement-panel.css.ts
Normal file
@ -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",
|
||||
},
|
||||
});
|
57
src/renderer/src/pages/achievements/achievement-panel.tsx
Normal file
57
src/renderer/src/pages/achievements/achievement-panel.tsx
Normal file
@ -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 (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.content}>
|
||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||
??? / ???
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements-points")}
|
||||
className={styles.link}
|
||||
>
|
||||
<small style={{ color: vars.color.warning }}>
|
||||
{t("how_to_earn_achievements_points")}
|
||||
</small>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.content}>
|
||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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<UserInfo, "profileImageUrl" | "displayName">
|
||||
@ -93,7 +93,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
<h3>
|
||||
<button
|
||||
className={styles.subscriptionRequiredButton}
|
||||
onClick={handleClickOpenCheckout}
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
>
|
||||
{t("subscription_needed")}
|
||||
</button>
|
||||
@ -171,38 +171,6 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function AchievementList({ achievements }: AchievementListProps) {
|
||||
const { t } = useTranslation("achievement");
|
||||
const { formatDateTime } = useDate();
|
||||
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
{achievements.map((achievement, index) => (
|
||||
<li key={index} className={styles.listItem} style={{ display: "flex" }}>
|
||||
<img
|
||||
className={styles.listItemImage({
|
||||
unlocked: achievement.unlocked,
|
||||
})}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h4>{achievement.displayName}</h4>
|
||||
<p>{achievement.description}</p>
|
||||
</div>
|
||||
{achievement.unlockTime && (
|
||||
<div style={{ whiteSpace: "nowrap" }}>
|
||||
<small>{t("unlocked_at")}</small>
|
||||
<p>{formatDateTime(achievement.unlockTime)}</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function AchievementsContent({
|
||||
otherUser,
|
||||
comparedAchievements,
|
||||
@ -355,9 +323,15 @@ export function AchievementsContent({
|
||||
)}
|
||||
|
||||
{otherUser ? (
|
||||
<ComparedAchievementList achievements={comparedAchievements!} />
|
||||
<>
|
||||
<ComparedAchievementPanel achievements={comparedAchievements!} />
|
||||
<ComparedAchievementList achievements={comparedAchievements!} />
|
||||
</>
|
||||
) : (
|
||||
<AchievementList achievements={achievements!} />
|
||||
<>
|
||||
<AchievementPanel achievements={achievements!} />
|
||||
<AchievementList achievements={achievements!} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
@ -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"
|
||||
/>
|
||||
<div>
|
||||
<h4>{achievement.displayName}</h4>
|
||||
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
{achievement.hidden && (
|
||||
<span
|
||||
style={{ display: "flex" }}
|
||||
title={t("hidden_achievement_tooltip")}
|
||||
>
|
||||
<EyeClosedIcon size={12} />
|
||||
</span>
|
||||
)}
|
||||
{achievement.displayName}
|
||||
</h4>
|
||||
<p>{achievement.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -58,11 +74,9 @@ export function ComparedAchievementList({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
title={formatDateTime(achievement.ownerStat.unlockTime!)}
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
<small>
|
||||
{formatDateTime(achievement.ownerStat.unlockTime!)}
|
||||
</small>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
@ -86,11 +100,9 @@ export function ComparedAchievementList({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
title={formatDateTime(achievement.targetStat.unlockTime!)}
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
<small>
|
||||
{formatDateTime(achievement.targetStat.unlockTime!)}
|
||||
</small>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./achievement-panel.css";
|
||||
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { ComparedAchievements } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
|
||||
export interface ComparedAchievementPanelProps {
|
||||
achievements: ComparedAchievements;
|
||||
}
|
||||
|
||||
export function ComparedAchievementPanel({
|
||||
achievements,
|
||||
}: ComparedAchievementPanelProps) {
|
||||
const { t } = useTranslation("achievement");
|
||||
const { hasActiveSubscription } = useUserDetails();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.panel}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: hasActiveSubscription ? "3fr 1fr 1fr" : "3fr 2fr",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{t("available_points")} <HydraIcon width={20} height={20} />{" "}
|
||||
{achievements.achievementsPointsTotal}
|
||||
</div>
|
||||
{hasActiveSubscription && (
|
||||
<div className={styles.content}>
|
||||
<HydraIcon width={20} height={20} />
|
||||
{achievements.owner.achievementsPointsEarnedSum ?? 0}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<HydraIcon width={20} height={20} />
|
||||
{achievements.target.achievementsPointsEarnedSum}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -13,7 +13,7 @@ import "./catalogue.scss";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { downloadSourcesTable } from "@renderer/dexie";
|
||||
import { FilterSection } from "./filter-section";
|
||||
import { setFilters } from "@renderer/features";
|
||||
import { setFilters, setPage } from "@renderer/features";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { Pagination } from "./pagination";
|
||||
@ -42,12 +42,11 @@ export default function Catalogue() {
|
||||
|
||||
const [results, setResults] = useState<any[]>([]);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [itemsCount, setItemsCount] = useState(0);
|
||||
|
||||
const { formatNumber } = useFormat();
|
||||
|
||||
const { filters } = useAppSelector((state) => state.catalogueSearch);
|
||||
const { filters, page } = useAppSelector((state) => state.catalogueSearch);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -103,10 +102,6 @@ export default function Catalogue() {
|
||||
}));
|
||||
}, [steamGenresMapping, filters.genres]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [filters]);
|
||||
|
||||
const steamUserTagsFilterItems = useMemo(() => {
|
||||
if (!steamUserTags[language]) return [];
|
||||
|
||||
@ -240,7 +235,7 @@ export default function Catalogue() {
|
||||
}}
|
||||
>
|
||||
{groupedFilters.map((filter) => (
|
||||
<li key={filter.label}>
|
||||
<li key={`${filter.key}-${filter.value}`}>
|
||||
<FilterItem
|
||||
filter={filter.label}
|
||||
orbColor={filter.orbColor}
|
||||
@ -308,7 +303,7 @@ export default function Catalogue() {
|
||||
<Pagination
|
||||
page={page}
|
||||
totalPages={Math.ceil(itemsCount / PAGE_SIZE)}
|
||||
onPageChange={setPage}
|
||||
onPageChange={(page) => dispatch(setPage(page))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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({
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedDownloader != null &&
|
||||
selectedDownloader !== Downloader.Torrent && (
|
||||
<p style={{ marginTop: `${SPACING_UNIT}px` }}>
|
||||
<span style={{ color: vars.color.warning }}>
|
||||
{t("warning")}
|
||||
</span>{" "}
|
||||
{t("hydra_needs_to_remain_open")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -21,6 +21,7 @@ import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
|
||||
const fakeAchievements: UserAchievement[] = [
|
||||
{
|
||||
@ -67,15 +68,10 @@ export function Sidebar() {
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("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 && (
|
||||
<button
|
||||
className={styles.subscriptionRequiredButton}
|
||||
onClick={handleClickOpenCheckout}
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
>
|
||||
<CloudOfflineIcon size={16} />
|
||||
<span>{t("achievements_not_sync")}</span>
|
||||
|
@ -2,7 +2,7 @@ import { userProfileContext } from "@renderer/context";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import * as styles from "./profile-content.css";
|
||||
import { Avatar, Link } from "@renderer/components";
|
||||
|
||||
@ -13,6 +13,21 @@ export function FriendsBox() {
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const getGameImage = (game: { iconUrl: string | null; title: string }) => {
|
||||
if (game.iconUrl) {
|
||||
return (
|
||||
<img
|
||||
alt={game.title}
|
||||
width={16}
|
||||
style={{ borderRadius: 4 }}
|
||||
src={game.iconUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SteamLogo width={16} height={16} />;
|
||||
};
|
||||
|
||||
if (!userProfile?.friends.length) return null;
|
||||
|
||||
return (
|
||||
@ -27,7 +42,14 @@ export function FriendsBox() {
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userProfile?.friends.map((friend) => (
|
||||
<li key={friend.id}>
|
||||
<li
|
||||
key={friend.id}
|
||||
title={
|
||||
friend.currentGame
|
||||
? t("playing", { game: friend.currentGame.title })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Link to={`/profile/${friend.id}`} className={styles.listItem}>
|
||||
<Avatar
|
||||
size={32}
|
||||
@ -35,7 +57,19 @@ export function FriendsBox() {
|
||||
alt={friend.displayName}
|
||||
/>
|
||||
|
||||
<span className={styles.friendName}>{friend.displayName}</span>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 4 }}
|
||||
>
|
||||
<span className={styles.friendName}>
|
||||
{friend.displayName}
|
||||
</span>
|
||||
{friend.currentGame && (
|
||||
<div style={{ display: "flex", gap: 4 }}>
|
||||
{getGameImage(friend.currentGame)}
|
||||
<small>{friend.currentGame.title}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
@ -105,6 +105,22 @@ export const listItem = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const statsListItem = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
transition: "all ease 0.1s",
|
||||
color: vars.color.muted,
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
textDecoration: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export const gamesGrid = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
@ -203,3 +219,12 @@ export const achievementsProgressBar = style({
|
||||
borderRadius: "4px",
|
||||
},
|
||||
});
|
||||
|
||||
export const link = style({
|
||||
textAlign: "start",
|
||||
color: vars.color.body,
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
@ -21,6 +21,8 @@ import {
|
||||
formatDownloadProgress,
|
||||
} from "@renderer/helpers";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import { UserStatsBox } from "./user-stats-box";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
|
||||
export function ProfileContent() {
|
||||
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
||||
@ -135,6 +137,7 @@ export function ProfileContent() {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
}}
|
||||
title={game.title}
|
||||
className={styles.game}
|
||||
>
|
||||
<button
|
||||
@ -155,7 +158,7 @@ export function ProfileContent() {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background:
|
||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.7) 20%, transparent 100%)",
|
||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.75) 25%, transparent 100%)",
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
@ -185,6 +188,22 @@ export function ProfileContent() {
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
gap: 8,
|
||||
marginBottom: 4,
|
||||
color: vars.color.muted,
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
{numberFormatter.format(
|
||||
game.achievementsPointsEarnedSum
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@ -249,6 +268,7 @@ export function ProfileContent() {
|
||||
|
||||
{shouldShowRightContent && (
|
||||
<div className={styles.rightContent}>
|
||||
<UserStatsBox />
|
||||
<RecentGamesBox />
|
||||
<FriendsBox />
|
||||
|
||||
|
@ -0,0 +1,125 @@
|
||||
import * as styles from "./profile-content.css";
|
||||
import { useCallback, useContext } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
export function UserStatsBox() {
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
const { userStats, isMe } = useContext(userProfileContext);
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(playTimeInSeconds: number) => {
|
||||
const seconds = playTimeInSeconds;
|
||||
const minutes = seconds / 60;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes", {
|
||||
amount: minutes.toFixed(0),
|
||||
});
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
},
|
||||
[numberFormatter, t]
|
||||
);
|
||||
|
||||
if (!userStats) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>{t("stats")}</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||
<li className={styles.statsListItem}>
|
||||
<h3 className={styles.listItemTitle}>
|
||||
{t("achievements_unlocked")}
|
||||
</h3>
|
||||
{userStats.unlockedAchievementSum !== undefined ? (
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<p className={styles.listItemDescription}>
|
||||
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
||||
{t("achievements")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
className={styles.link}
|
||||
>
|
||||
<small style={{ color: vars.color.warning }}>
|
||||
{t("show_achievements_on_profile")}
|
||||
</small>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
|
||||
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
||||
<li className={styles.statsListItem}>
|
||||
<h3 className={styles.listItemTitle}>{t("earned_points")}</h3>
|
||||
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<p className={styles.listItemDescription}>
|
||||
<HydraIcon width={20} height={20} />
|
||||
{numberFormatter.format(
|
||||
userStats.achievementsPointsEarnedSum.value
|
||||
)}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile:
|
||||
userStats.achievementsPointsEarnedSum.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements-points")}
|
||||
className={styles.link}
|
||||
>
|
||||
<small style={{ color: vars.color.warning }}>
|
||||
{t("show_points_on_profile")}
|
||||
</small>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li className={styles.statsListItem}>
|
||||
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<p className={styles.listItemDescription}>
|
||||
<ClockIcon />
|
||||
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile: userStats.totalPlayTimeInSeconds.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
});
|
@ -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 (
|
||||
<Modal visible={visible} title={t("hydra_cloud")} onClose={onClose}>
|
||||
<div
|
||||
data-hydra-cloud-feature={feature}
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "500px",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
{t("hydra_cloud_feature_found")}
|
||||
<Button onClick={handleClickOpenCheckout}>{t("learn_more")}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -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,
|
||||
},
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user