Skip to content

Commit

Permalink
Merge pull request #1354 from hydralauncher/feat/game-card-animation
Browse files Browse the repository at this point in the history
feat: game card stats animation
  • Loading branch information
zamitto authored Dec 28, 2024
2 parents 4e28292 + db2688f commit 3bef263
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 187 deletions.
4 changes: 2 additions & 2 deletions src/main/services/download/download-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export class DownloadManager {
private static async getDownloadPayload(game: Game) {
switch (game.downloader) {
case Downloader.Gofile: {
const id = game!.uri!.split("/").pop();
const id = game.uri!.split("/").pop();

const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!);
Expand All @@ -258,7 +258,7 @@ export class DownloadManager {
};
}
case Downloader.PixelDrain: {
const id = game!.uri!.split("/").pop();
const id = game.uri!.split("/").pop();

return {
action: "start",
Expand Down
9 changes: 6 additions & 3 deletions src/renderer/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
227 changes: 45 additions & 182 deletions src/renderer/src/pages/profile/profile-content/profile-content.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
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";
import { steamUrlBuilder } from "@shared";
import { SPACING_UNIT, vars } from "@renderer/theme.css";

import { SPACING_UNIT } from "@renderer/theme.css";
import * as styles from "./profile-content.css";
import { ClockIcon, TelescopeIcon, TrophyIcon } from "@primer/octicons-react";
import { TelescopeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { LockedProfile } from "./locked-profile";
import { ReportProfile } from "../report-profile/report-profile";
import { FriendsBox } from "./friends-box";
import { RecentGamesBox } from "./recent-games-box";
import type { UserGame } from "@types";
import {
buildGameAchievementPath,
buildGameDetailsPath,
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";
import { UserLibraryGameCard } from "./user-library-game-card";

const GAME_STATS_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();

Expand All @@ -39,49 +35,42 @@ export function ProfileContent() {
}
}, [userProfile, dispatch]);

const { numberFormatter } = useFormat();

const navigate = useNavigate();
const handleOnMouseEnterGameCard = () => {
setIsAnimationRunning(false);
};

const usersAreFriends = useMemo(() => {
return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]);
const handleOnMouseLeaveGameCard = () => {
setIsAnimationRunning(true);
};

const buildUserGameDetailsPath = useCallback(
(game: UserGame) => {
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
return buildGameDetailsPath({
...game,
objectId: game.objectId,
});
useEffect(() => {
let zero = performance.now();
if (!isAnimationRunning) return;

statsAnimation.current = requestAnimationFrame(
function animateGameStats(time) {
if (time - zero <= GAME_STATS_ANIMATION_DURATION_IN_MS) {
statsAnimation.current = requestAnimationFrame(animateGameStats);
} else {
setStatsIndex((index) => index + 1);
zero = performance.now();
statsAnimation.current = requestAnimationFrame(animateGameStats);
}
}
);

const userParams = userProfile
? {
userId: userProfile.id,
}
: undefined;

return buildGameAchievementPath({ ...game }, userParams);
},
[userProfile]
);
return () => {
cancelAnimationFrame(statsAnimation.current);
};
}, [setStatsIndex, isAnimationRunning]);

const formatPlayTime = useCallback(
(playTimeInSeconds = 0) => {
const minutes = playTimeInSeconds / 60;
const { numberFormatter } = useFormat();

if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
return t("amount_minutes", {
amount: minutes.toFixed(0),
});
}
const navigate = useNavigate();

const hours = minutes / 60;
return t("amount_hours", { amount: numberFormatter.format(hours) });
},
[numberFormatter, t]
);
const usersAreFriends = useMemo(() => {
return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]);

const content = useMemo(() => {
if (!userProfile) return null;
Expand Down Expand Up @@ -129,137 +118,13 @@ export function ProfileContent() {

<ul className={styles.gamesGrid}>
{userProfile?.libraryGames?.map((game) => (
<li
<UserLibraryGameCard
game={game}
key={game.objectId}
style={{
borderRadius: 4,
overflow: "hidden",
position: "relative",
display: "flex",
}}
title={game.title}
className={styles.game}
>
<button
type="button"
style={{
cursor: "pointer",
}}
className={styles.gameCover}
onClick={() => navigate(buildUserGameDetailsPath(game))}
>
<div
style={{
position: "absolute",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "space-between",
height: "100%",
width: "100%",
background:
"linear-gradient(0deg, rgba(0, 0, 0, 0.75) 25%, transparent 100%)",
padding: 8,
}}
>
<small
style={{
backgroundColor: vars.color.background,
color: vars.color.muted,
border: `solid 1px ${vars.color.border}`,
borderRadius: 4,
display: "flex",
alignItems: "center",
gap: 4,
padding: "4px",
}}
>
<ClockIcon size={11} />
{formatPlayTime(game.playTimeInSeconds)}
</small>

{userProfile.hasActiveSubscription &&
game.achievementCount > 0 && (
<div
style={{
color: "#fff",
width: "100%",
display: "flex",
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",
justifyContent: "space-between",
marginBottom: 8,
color: vars.color.muted,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<TrophyIcon size={13} />
<span>
{game.unlockedAchievementCount} /{" "}
{game.achievementCount}
</span>
</div>

<span>
{formatDownloadProgress(
game.unlockedAchievementCount /
game.achievementCount
)}
</span>
</div>

<progress
max={1}
value={
game.unlockedAchievementCount /
game.achievementCount
}
className={styles.achievementsProgressBar}
/>
</div>
)}
</div>

<img
src={steamUrlBuilder.cover(game.objectId)}
alt={game.title}
style={{
objectFit: "cover",
borderRadius: 4,
width: "100%",
height: "100%",
minWidth: "100%",
minHeight: "100%",
}}
/>
</button>
</li>
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
/>
))}
</ul>
</>
Expand All @@ -271,7 +136,6 @@ export function ProfileContent() {
<UserStatsBox />
<RecentGamesBox />
<FriendsBox />

<ReportProfile />
</div>
)}
Expand All @@ -284,9 +148,8 @@ export function ProfileContent() {
userStats,
numberFormatter,
t,
buildUserGameDetailsPath,
formatPlayTime,
navigate,
statsIndex,
]);

return (
Expand Down
Loading

0 comments on commit 3bef263

Please sign in to comment.