feat: ui improvement

This commit is contained in:
Zamitto 2024-12-22 18:56:24 -03:00
parent 313ebc6055
commit c8566dd2be
12 changed files with 181 additions and 64 deletions

View File

@ -168,8 +168,7 @@
"select_folder": "Select folder", "select_folder": "Select folder",
"backup_from": "Backup from {{date}}", "backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set", "custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected", "no_directory_selected": "No directory selected"
"available_points": "Available points:"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",
@ -367,11 +366,14 @@
"uploading_banner": "Uploading banner…", "uploading_banner": "Uploading banner…",
"background_image_updated": "Background image updated", "background_image_updated": "Background image updated",
"stats": "Stats", "stats": "Stats",
"achievements": "Achievements", "achievements": "achievements",
"games": "Games", "games": "Games",
"top_percentile": "Top {{percentile}}%", "top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Ranking is updated weekly", "ranking_updated_weekly": "Ranking is updated weekly",
"playing": "Playing {{game}}" "playing": "Playing {{game}}",
"achievements_unlocked": "Achievements Unlocked",
"earned_points": "Earned points",
"show_achievements_on_profile": "Show your achievements and earned points on your profile"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Achievement unlocked", "achievement_unlocked": "Achievement unlocked",
@ -383,7 +385,9 @@
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievements", "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", "hidden_achievement_tooltip": "This is a hidden achievement",
"achievement_earn_points": "Earn {{points}} points with this achievement" "achievement_earn_points": "Earn {{points}} points with this achievement",
"earned_points": "Earned points:",
"available_points": "Available points:"
}, },
"hydra_cloud": { "hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Subscription", "subscription_tour_title": "Hydra Cloud Subscription",

View File

@ -164,8 +164,7 @@
"select_folder": "Selecione a pasta", "select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup", "manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar", "clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado", "no_directory_selected": "Nenhum diretório selecionado"
"available_points": "Pontos disponíveis:"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",
@ -365,10 +364,13 @@
"uploading_banner": "Carregando banner…", "uploading_banner": "Carregando banner…",
"background_image_updated": "Imagem de fundo salva", "background_image_updated": "Imagem de fundo salva",
"stats": "Estatísticas", "stats": "Estatísticas",
"achievements": "Conquistas", "achievements": "conquistas",
"games": "Jogos", "games": "Jogos",
"ranking_updated_weekly": "Ranking é atualizado semanalmente", "ranking_updated_weekly": "O ranking é atualizado semanalmente",
"playing": "Jogando {{game}}" "playing": "Jogando {{game}}",
"achievements_unlocked": "Conquistas desbloqueadas",
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Exiba suas conquistas e pontos no perfil"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Conquista desbloqueada", "achievement_unlocked": "Conquista desbloqueada",
@ -380,7 +382,9 @@
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas", "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", "hidden_achievement_tooltip": "Está é uma conquista oculta",
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista" "achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
"earned_points": "Pontos ganhos:",
"available_points": "Pontos disponíveis:"
}, },
"hydra_cloud": { "hydra_cloud": {
"subscription_tour_title": "Assinatura Hydra Cloud", "subscription_tour_title": "Assinatura Hydra Cloud",

View File

@ -13,6 +13,9 @@ const getComparedUnlockedAchievements = async (
where: { id: 1 }, where: { id: 1 },
}); });
const showHiddenAchievementsDescription =
userPreferences?.showHiddenAchievementsDescription || false;
return HydraApi.get<ComparedAchievements>( return HydraApi.get<ComparedAchievements>(
`/users/${userId}/games/achievements/compare`, `/users/${userId}/games/achievements/compare`,
{ {
@ -21,7 +24,8 @@ const getComparedUnlockedAchievements = async (
language: userPreferences?.language || "en", language: userPreferences?.language || "en",
} }
).then((achievements) => { ).then((achievements) => {
const sortedAchievements = achievements.achievements.sort((a, b) => { 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 1; if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1;
if (a.targetStat.unlocked && b.targetStat.unlocked) { if (a.targetStat.unlocked && b.targetStat.unlocked) {
@ -29,6 +33,25 @@ const getComparedUnlockedAchievements = async (
} }
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 { return {

View File

@ -4,13 +4,25 @@ import { gameDetailsContext } from "@renderer/context";
import * as styles from "./achievement-panel.css"; import * as styles from "./achievement-panel.css";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { UserAchievement } from "@types";
export interface HeroPanelProps { export interface AchievementPanelProps {
isHeaderStuck: boolean; achievements: UserAchievement[];
} }
export function AchievementPanel({ isHeaderStuck }: HeroPanelProps) { export function AchievementPanel({ achievements }: AchievementPanelProps) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("achievement");
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
);
const {} = useContext(gameDetailsContext); const {} = useContext(gameDetailsContext);
@ -18,7 +30,8 @@ export function AchievementPanel({ isHeaderStuck }: HeroPanelProps) {
<> <>
<div className={styles.panel}> <div className={styles.panel}>
<div className={styles.content}> <div className={styles.content}>
Pontos desbloqueados: <HydraIcon width={20} height={20} /> 69/420 {t("earned_points")} <HydraIcon width={20} height={20} />
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
</div> </div>
</div> </div>
</> </>

View File

@ -329,7 +329,7 @@ export function AchievementsContent({
</> </>
) : ( ) : (
<> <>
<AchievementPanel isHeaderStuck={false} /> <AchievementPanel achievements={achievements!} />
<AchievementList achievements={achievements!} /> <AchievementList achievements={achievements!} />
</> </>
)} )}

View File

@ -1,8 +1,13 @@
import type { ComparedAchievements } from "@types"; import type { ComparedAchievements } from "@types";
import * as styles from "./achievements.css"; 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 { useDate } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { useTranslation } from "react-i18next";
export interface ComparedAchievementListProps { export interface ComparedAchievementListProps {
achievements: ComparedAchievements; achievements: ComparedAchievements;
@ -11,6 +16,7 @@ export interface ComparedAchievementListProps {
export function ComparedAchievementList({ export function ComparedAchievementList({
achievements, achievements,
}: ComparedAchievementListProps) { }: ComparedAchievementListProps) {
const { t } = useTranslation("achievement");
const { formatDateTime } = useDate(); const { formatDateTime } = useDate();
return ( return (
@ -43,7 +49,17 @@ export function ComparedAchievementList({
loading="lazy" loading="lazy"
/> />
<div> <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> <p>{achievement.description}</p>
</div> </div>
</div> </div>

View File

@ -14,7 +14,7 @@ export interface ComparedAchievementPanelProps {
export function ComparedAchievementPanel({ export function ComparedAchievementPanel({
achievements, achievements,
}: ComparedAchievementPanelProps) { }: ComparedAchievementPanelProps) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("achievement");
const {} = useContext(gameDetailsContext); const {} = useContext(gameDetailsContext);

View File

@ -41,9 +41,7 @@ export function FriendsBox() {
{friend.displayName} {friend.displayName}
</span> </span>
{friend.currentGame && ( {friend.currentGame && (
<Link to={buildGameDetailsPath({ ...friend.currentGame })}>
<p>{t("playing", { game: friend.currentGame.title })}</p> <p>{t("playing", { game: friend.currentGame.title })}</p>
</Link>
)} )}
</div> </div>
</Link> </Link>

View File

@ -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({ export const gamesGrid = style({
listStyle: "none", listStyle: "none",
margin: "0", margin: "0",

View File

@ -22,6 +22,7 @@ import {
} from "@renderer/helpers"; } from "@renderer/helpers";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { UserStatsBox } from "./user-stats-box"; import { UserStatsBox } from "./user-stats-box";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
export function ProfileContent() { export function ProfileContent() {
const { userProfile, isMe, userStats } = useContext(userProfileContext); const { userProfile, isMe, userStats } = useContext(userProfileContext);
@ -157,7 +158,7 @@ export function ProfileContent() {
height: "100%", height: "100%",
width: "100%", width: "100%",
background: 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, padding: 8,
}} }}
> >
@ -187,6 +188,22 @@ export function ProfileContent() {
flexDirection: "column", 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 <div
style={{ style={{
display: "flex", display: "flex",

View File

@ -6,14 +6,13 @@ import { useFormat } from "@renderer/hooks";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription"; import { useSubscription } from "@renderer/hooks/use-subscription";
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
import { vars } from "@renderer/theme.css";
export function UserStatsBox() { export function UserStatsBox() {
const { showHydraCloudModal } = useSubscription(); const { showHydraCloudModal } = useSubscription();
const { userStats, isMe } = useContext(userProfileContext);
const { userStats } = useContext(userProfileContext);
const { t } = useTranslation("user_profile"); const { t } = useTranslation("user_profile");
const { numberFormatter } = useFormat(); const { numberFormatter } = useFormat();
const formatPlayTime = useCallback( const formatPlayTime = useCallback(
@ -43,22 +42,46 @@ export function UserStatsBox() {
<div className={styles.box}> <div className={styles.box}>
<ul className={styles.list}> <ul className={styles.list}>
<li> {(isMe || userStats.unlockedAchievementSum !== undefined) && (
<h3 className={styles.listItemTitle}>{t("achievements")}</h3> <li className={styles.statsListItem}>
{userStats.achievementsPointsEarnedSum !== undefined ? ( <h3 className={styles.listItemTitle}>
<> {t("achievements_unlocked")}
</h3>
{userStats.unlockedAchievementSum !== undefined ? (
<div <div
style={{ display: "flex", justifyContent: "space-between" }} style={{ display: "flex", justifyContent: "space-between" }}
> >
<p <p className={styles.listItemDescription}>
style={{ <TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
display: "flex", {t("achievements")}
alignItems: "center", </p>
gap: "4px", </div>
}} ) : (
<button
type="button"
onClick={showHydraCloudModal}
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} /> <HydraIcon width={20} height={20} />
{userStats.achievementsPointsEarnedSum.value} {numberFormatter.format(
userStats.achievementsPointsEarnedSum.value
)}
</p> </p>
<p title={t("ranking_updated_weekly")}> <p title={t("ranking_updated_weekly")}>
{t("top_percentile", { {t("top_percentile", {
@ -67,25 +90,27 @@ export function UserStatsBox() {
})} })}
</p> </p>
</div> </div>
<p>Unlock count: {userStats.unlockedAchievementSum}</p>
</>
) : ( ) : (
<button <button
type="button" type="button"
onClick={showHydraCloudModal} onClick={showHydraCloudModal}
className={styles.link} className={styles.link}
> >
<small> <small style={{ color: vars.color.warning }}>
Saiba como exibir suas conquistas e pontos no perfil {t("show_achievements_on_profile")}
</small> </small>
</button> </button>
)} )}
</li> </li>
)}
<li> <li className={styles.statsListItem}>
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3> <h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
<div style={{ display: "flex", justifyContent: "space-between" }}> <div style={{ display: "flex", justifyContent: "space-between" }}>
<p>{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}</p> <p className={styles.listItemDescription}>
<ClockIcon />
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
</p>
<p title={t("ranking_updated_weekly")}> <p title={t("ranking_updated_weekly")}>
{t("top_percentile", { {t("top_percentile", {
percentile: userStats.totalPlayTimeInSeconds.topPercentile, percentile: userStats.totalPlayTimeInSeconds.topPercentile,

View File

@ -99,6 +99,7 @@ export interface UserGame {
lastTimePlayed: Date | null; lastTimePlayed: Date | null;
unlockedAchievementCount: number; unlockedAchievementCount: number;
achievementCount: number; achievementCount: number;
achievementsPointsEarnedSum: number;
} }
export interface DownloadQueue { export interface DownloadQueue {