From f5d5aa39dc192e4138c98a75077558d4e117cde5 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Wed, 25 Dec 2024 20:28:56 -0300
Subject: [PATCH 1/4] feat: game card animation
---
.../services/download/download-manager.ts | 4 +-
.../profile-content/profile-content.tsx | 168 +--------------
.../user-library-game-card.tsx | 204 ++++++++++++++++++
3 files changed, 210 insertions(+), 166 deletions(-)
create mode 100644 src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts
index 80a3f6fb..0d9f5cbb 100644
--- a/src/main/services/download/download-manager.ts
+++ b/src/main/services/download/download-manager.ts
@@ -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!);
@@ -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",
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 2217d569..ed63029b 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -3,26 +3,18 @@ import { useCallback, useContext, useEffect, useMemo } 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";
export function ProfileContent() {
const { userProfile, isMe, userStats } = useContext(userProfileContext);
@@ -47,26 +39,6 @@ export function ProfileContent() {
return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]);
- const buildUserGameDetailsPath = useCallback(
- (game: UserGame) => {
- if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
- return buildGameDetailsPath({
- ...game,
- objectId: game.objectId,
- });
- }
-
- const userParams = userProfile
- ? {
- userId: userProfile.id,
- }
- : undefined;
-
- return buildGameAchievementPath({ ...game }, userParams);
- },
- [userProfile]
- );
-
const formatPlayTime = useCallback(
(playTimeInSeconds = 0) => {
const minutes = playTimeInSeconds / 60;
@@ -129,137 +101,7 @@ export function ProfileContent() {
{userProfile?.libraryGames?.map((game) => (
- -
-
-
+
))}
>
@@ -271,7 +113,6 @@ export function ProfileContent() {
-
)}
@@ -284,7 +125,6 @@ export function ProfileContent() {
userStats,
numberFormatter,
t,
- buildUserGameDetailsPath,
formatPlayTime,
navigate,
]);
diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
new file mode 100644
index 00000000..8f437a1d
--- /dev/null
+++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
@@ -0,0 +1,204 @@
+import { UserGame } from "@types";
+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 } from "react";
+import {
+ buildGameAchievementPath,
+ buildGameDetailsPath,
+ formatDownloadProgress,
+} from "@renderer/helpers";
+import { userProfileContext } from "@renderer/context";
+import { vars } from "@renderer/theme.css";
+import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
+import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
+import { useTranslation } from "react-i18next";
+import { steamUrlBuilder } from "@shared";
+
+interface UserLibraryGameCardProps {
+ game: UserGame;
+}
+
+export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
+ const { userProfile } = useContext(userProfileContext);
+ const { t } = useTranslation("user_profile");
+ const { numberFormatter } = useFormat();
+ const navigate = useNavigate();
+
+ // const handleCloseClick = useCallback(() => {
+ // setIsClosing(true);
+ // const zero = performance.now();
+
+ // requestAnimationFrame(function animateClosing(time) {
+ // if (time - zero <= 400) {
+ // requestAnimationFrame(animateClosing);
+ // } else {
+ // onClose();
+ // setIsClosing(false);
+ // }
+ // });
+ // }, [onClose]);
+
+ const buildUserGameDetailsPath = useCallback(
+ (game: UserGame) => {
+ if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
+ return buildGameDetailsPath({
+ ...game,
+ objectId: game.objectId,
+ });
+ }
+
+ const userParams = userProfile
+ ? {
+ userId: userProfile.id,
+ }
+ : undefined;
+
+ return buildGameAchievementPath({ ...game }, userParams);
+ },
+ [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]
+ );
+
+ return (
+
+
+
+ );
+}
From ec289fe4c7093b63bcba308c6f801d47a7f6e4de Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 26 Dec 2024 15:06:49 -0300
Subject: [PATCH 2/4] feat: animation
---
.../user-library-game-card.tsx | 131 +++++++++++-------
1 file changed, 82 insertions(+), 49 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
index 8f437a1d..d35a4d30 100644
--- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
+++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
@@ -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 } from "react";
+import { useCallback, useContext, useEffect, useState } from "react";
import {
buildGameAchievementPath,
buildGameDetailsPath,
@@ -26,19 +26,35 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
const { numberFormatter } = useFormat();
const navigate = useNavigate();
- // const handleCloseClick = useCallback(() => {
- // setIsClosing(true);
- // const zero = performance.now();
+ const [mediaIndex, setMediaIndex] = useState(0);
- // requestAnimationFrame(function animateClosing(time) {
- // if (time - zero <= 400) {
- // requestAnimationFrame(animateClosing);
- // } else {
- // onClose();
- // setIsClosing(false);
- // }
- // });
- // }, [onClose]);
+ 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 buildUserGameDetailsPath = useCallback(
(game: UserGame) => {
@@ -129,59 +145,76 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
+
+
+
+
+
+ {game.unlockedAchievementCount} / {game.achievementCount}
+
+
+
+
+ {formatDownloadProgress(
+ game.unlockedAchievementCount / game.achievementCount
+ )}
+
+
+
+
+
+
{game.achievementsPointsEarnedSum > 0 && (
{numberFormatter.format(game.achievementsPointsEarnedSum)}
)}
-
-
-
-
- {game.unlockedAchievementCount} / {game.achievementCount}
-
-
-
-
- {formatDownloadProgress(
- game.unlockedAchievementCount / game.achievementCount
- )}
-
-
-
-
)}
From 16eaf4261addbf1c282c6c2b1fda03161c85437b Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 26 Dec 2024 18:43:06 -0300
Subject: [PATCH 3/4] feat: animation and number format
---
src/renderer/src/helpers.ts | 9 +-
.../profile-content/profile-content.css.ts | 8 ++
.../profile-content/profile-content.tsx | 63 +++++---
.../user-library-game-card.tsx | 134 ++++++++----------
4 files changed, 119 insertions(+), 95 deletions(-)
diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts
index a241bf47..972cdfc9 100644
--- a/src/renderer/src/helpers.ts
+++ b/src/renderer/src/helpers.ts
@@ -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) => {
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts
index cc462a95..847e9492 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts
@@ -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",
+});
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index ed63029b..a8f65f0f 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -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() {
{userProfile?.libraryGames?.map((game) => (
-
+
))}
>
@@ -125,8 +148,8 @@ export function ProfileContent() {
userStats,
numberFormatter,
t,
- formatPlayTime,
navigate,
+ statsIndex,
]);
return (
diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
index d35a4d30..b61935f5 100644
--- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
+++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
@@ -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 (
@@ -147,33 +140,32 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
style={{
width: "100%",
display: "flex",
- overflow: "hidden",
+ flexDirection: "column",
}}
>
@@ -182,39 +174,37 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
-
- {formatDownloadProgress(
- game.unlockedAchievementCount / game.achievementCount
- )}
-
+ {game.achievementsPointsEarnedSum > 0 && (
+
+
+ {formatAchievementPoints(
+ game.achievementsPointsEarnedSum
+ )}
+
+ )}
-
+
+ {formatDownloadProgress(
+ game.unlockedAchievementCount / game.achievementCount,
+ 1
+ )}
+
- {game.achievementsPointsEarnedSum > 0 && (
-
-
- {numberFormatter.format(game.achievementsPointsEarnedSum)}
-
- )}
+
)}
From db2688f3a70c53cb89b029d93419111c2fe289ff Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Sat, 28 Dec 2024 11:28:43 -0300
Subject: [PATCH 4/4] feat: rename variable
---
.../pages/profile/profile-content/profile-content.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index a8f65f0f..951eb41b 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -15,7 +15,7 @@ import { RecentGamesBox } from "./recent-games-box";
import { UserStatsBox } from "./user-stats-box";
import { UserLibraryGameCard } from "./user-library-game-card";
-const GAME_STAT_ANIMATION_DURATION_IN_MS = 3500;
+const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
export function ProfileContent() {
const { userProfile, isMe, userStats } = useContext(userProfileContext);
@@ -48,13 +48,13 @@ export function ProfileContent() {
if (!isAnimationRunning) return;
statsAnimation.current = requestAnimationFrame(
- function animateClosing(time) {
- if (time - zero <= GAME_STAT_ANIMATION_DURATION_IN_MS) {
- statsAnimation.current = requestAnimationFrame(animateClosing);
+ 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(animateClosing);
+ statsAnimation.current = requestAnimationFrame(animateGameStats);
}
}
);