mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +03:00
feat: animation and number format
This commit is contained in:
parent
ec289fe4c7
commit
16eaf4261a
@ -2,14 +2,17 @@ import type { GameShop } from "@types";
|
||||
|
||||
import Color from "color";
|
||||
|
||||
export const formatDownloadProgress = (progress?: number) => {
|
||||
export const formatDownloadProgress = (
|
||||
progress?: number,
|
||||
fractionDigits?: number
|
||||
) => {
|
||||
if (!progress) return "0%";
|
||||
const progressPercentage = progress * 100;
|
||||
|
||||
if (Number(progressPercentage.toFixed(2)) % 1 === 0)
|
||||
if (Number(progressPercentage.toFixed(fractionDigits ?? 2)) % 1 === 0)
|
||||
return `${Math.floor(progressPercentage)}%`;
|
||||
|
||||
return `${progressPercentage.toFixed(2)}%`;
|
||||
return `${progressPercentage.toFixed(fractionDigits ?? 2)}%`;
|
||||
};
|
||||
|
||||
export const getSteamLanguage = (language: string) => {
|
||||
|
@ -228,3 +228,11 @@ export const link = style({
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
||||
export const gameCardStats = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transition: "transform 0.5s ease-in-out",
|
||||
flexShrink: "0",
|
||||
flexGrow: "0",
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useCallback, useContext, useEffect, useMemo } from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
@ -12,12 +12,16 @@ import { LockedProfile } from "./locked-profile";
|
||||
import { ReportProfile } from "../report-profile/report-profile";
|
||||
import { FriendsBox } from "./friends-box";
|
||||
import { RecentGamesBox } from "./recent-games-box";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import { UserStatsBox } from "./user-stats-box";
|
||||
import { UserLibraryGameCard } from "./user-library-game-card";
|
||||
|
||||
const GAME_STAT_ANIMATION_DURATION_IN_MS = 3500;
|
||||
|
||||
export function ProfileContent() {
|
||||
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
||||
const [statsIndex, setStatsIndex] = useState(0);
|
||||
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
||||
const statsAnimation = useRef(-1);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -31,6 +35,35 @@ export function ProfileContent() {
|
||||
}
|
||||
}, [userProfile, dispatch]);
|
||||
|
||||
const handleOnMouseEnterGameCard = () => {
|
||||
setIsAnimationRunning(false);
|
||||
};
|
||||
|
||||
const handleOnMouseLeaveGameCard = () => {
|
||||
setIsAnimationRunning(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let zero = performance.now();
|
||||
if (!isAnimationRunning) return;
|
||||
|
||||
statsAnimation.current = requestAnimationFrame(
|
||||
function animateClosing(time) {
|
||||
if (time - zero <= GAME_STAT_ANIMATION_DURATION_IN_MS) {
|
||||
statsAnimation.current = requestAnimationFrame(animateClosing);
|
||||
} else {
|
||||
setStatsIndex((index) => index + 1);
|
||||
zero = performance.now();
|
||||
statsAnimation.current = requestAnimationFrame(animateClosing);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(statsAnimation.current);
|
||||
};
|
||||
}, [setStatsIndex, isAnimationRunning]);
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -39,22 +72,6 @@ export function ProfileContent() {
|
||||
return userProfile?.relation?.status === "ACCEPTED";
|
||||
}, [userProfile]);
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(playTimeInSeconds = 0) => {
|
||||
const minutes = playTimeInSeconds / 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]
|
||||
);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (!userProfile) return null;
|
||||
|
||||
@ -101,7 +118,13 @@ export function ProfileContent() {
|
||||
|
||||
<ul className={styles.gamesGrid}>
|
||||
{userProfile?.libraryGames?.map((game) => (
|
||||
<UserLibraryGameCard game={game} key={game.objectId} />
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
key={game.objectId}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
@ -125,8 +148,8 @@ export function ProfileContent() {
|
||||
userStats,
|
||||
numberFormatter,
|
||||
t,
|
||||
formatPlayTime,
|
||||
navigate,
|
||||
statsIndex,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -3,7 +3,7 @@ import * as styles from "./profile-content.css";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useCallback, useContext } from "react";
|
||||
import {
|
||||
buildGameAchievementPath,
|
||||
buildGameDetailsPath,
|
||||
@ -18,43 +18,27 @@ import { steamUrlBuilder } from "@shared";
|
||||
|
||||
interface UserLibraryGameCardProps {
|
||||
game: UserGame;
|
||||
statIndex: number;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
}
|
||||
|
||||
export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
export function UserLibraryGameCard({
|
||||
game,
|
||||
statIndex,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: UserLibraryGameCardProps) {
|
||||
const { userProfile } = useContext(userProfileContext);
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { numberFormatter } = useFormat();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [mediaIndex, setMediaIndex] = useState(0);
|
||||
|
||||
const statsItemCount =
|
||||
Number(Boolean(game.achievementsPointsEarnedSum)) +
|
||||
Number(Boolean(game.unlockedAchievementCount));
|
||||
|
||||
console.log(game.title, statsItemCount);
|
||||
|
||||
useEffect(() => {
|
||||
if (statsItemCount <= 1) return;
|
||||
|
||||
let zero = performance.now();
|
||||
const animation = requestAnimationFrame(function animateClosing(time) {
|
||||
if (time - zero <= 4000) {
|
||||
requestAnimationFrame(animateClosing);
|
||||
} else {
|
||||
setMediaIndex((index) => {
|
||||
if (index === statsItemCount - 1) return 0;
|
||||
return index + 1;
|
||||
});
|
||||
zero = performance.now();
|
||||
requestAnimationFrame(animateClosing);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
};
|
||||
}, [setMediaIndex, statsItemCount]);
|
||||
const getStatsItemCount = useCallback(() => {
|
||||
let statsCount = 1;
|
||||
if (game.achievementsPointsEarnedSum > 0) statsCount++;
|
||||
return statsCount;
|
||||
}, [game]);
|
||||
|
||||
const buildUserGameDetailsPath = useCallback(
|
||||
(game: UserGame) => {
|
||||
@ -76,6 +60,14 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
[userProfile]
|
||||
);
|
||||
|
||||
const formatAchievementPoints = (number: number) => {
|
||||
if (number < 100_000) return numberFormatter.format(number);
|
||||
|
||||
if (number < 1_000_000) return `${(number / 1000).toFixed(1)}K`;
|
||||
|
||||
return `${(number / 1_000_000).toFixed(1)}M`;
|
||||
};
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(playTimeInSeconds = 0) => {
|
||||
const minutes = playTimeInSeconds / 60;
|
||||
@ -94,7 +86,8 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
|
||||
return (
|
||||
<li
|
||||
key={game.objectId}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
@ -122,7 +115,7 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background:
|
||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.75) 25%, transparent 100%)",
|
||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.70) 20%, transparent 100%)",
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
@ -143,22 +136,11 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
</small>
|
||||
|
||||
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
transition: "translate 0.5s ease-in-out",
|
||||
flexShrink: "0",
|
||||
flexGrow: "0",
|
||||
translate: `${-100 * mediaIndex}%`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@ -167,13 +149,23 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 8,
|
||||
color: vars.color.muted,
|
||||
overflow: "hidden",
|
||||
height: 18,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.gameCardStats}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
@ -182,9 +174,28 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
className={styles.gameCardStats}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 5,
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
{formatAchievementPoints(
|
||||
game.achievementsPointsEarnedSum
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
game.unlockedAchievementCount / game.achievementCount
|
||||
game.unlockedAchievementCount / game.achievementCount,
|
||||
1
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@ -195,27 +206,6 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
|
||||
className={styles.achievementsProgressBar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
gap: 8,
|
||||
width: "100%",
|
||||
translate: `${-100 * mediaIndex}%`,
|
||||
transition: "translate 0.5s ease-in-out",
|
||||
alignItems: "center",
|
||||
color: vars.color.muted,
|
||||
flexShrink: "0",
|
||||
flexGrow: "0",
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
{numberFormatter.format(game.achievementsPointsEarnedSum)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user