mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-15 04:32:13 +00:00
feat: update achievements page
This commit is contained in:
parent
8fb31e0e64
commit
fa026f82a6
9 changed files with 228 additions and 52 deletions
|
@ -333,6 +333,8 @@
|
||||||
"your_friend_code": "Your friend code:"
|
"your_friend_code": "Your friend code:"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked"
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
|
"user_achievements": "{{displayName}}'s Achievements",
|
||||||
|
"your_achievements": "Your Achievements"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -335,6 +335,8 @@
|
||||||
"your_friend_code": "Seu código de amigo:"
|
"your_friend_code": "Seu código de amigo:"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada"
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
"your_achievements": "Suas Conquistas",
|
||||||
|
"user_achievements": "Conquistas de {{displayName}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,9 +83,9 @@ export const getGameAchievements = async (
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
unlockTime: null,
|
unlockTime: null,
|
||||||
icongray,
|
icongray,
|
||||||
};
|
} as GameAchievement;
|
||||||
})
|
})
|
||||||
.sort((a: GameAchievement, b: GameAchievement) => {
|
.sort((a, b) => {
|
||||||
if (a.unlocked && !b.unlocked) return -1;
|
if (a.unlocked && !b.unlocked) return -1;
|
||||||
if (!a.unlocked && b.unlocked) return 1;
|
if (!a.unlocked && b.unlocked) return 1;
|
||||||
if (a.unlocked && b.unlocked) {
|
if (a.unlocked && b.unlocked) {
|
||||||
|
|
|
@ -53,7 +53,7 @@ const getPathFromCracker = (cracker: Cracker) => {
|
||||||
if (cracker === Cracker.onlineFix) {
|
if (cracker === Cracker.onlineFix) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(publicDocuments, Cracker.onlineFix),
|
folderPath: path.join(publicDocuments, "OnlineFix"),
|
||||||
fileLocation: ["Stats", "Achievements.ini"],
|
fileLocation: ["Stats", "Achievements.ini"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -34,5 +34,20 @@ export const buildGameDetailsPath = (
|
||||||
return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`;
|
return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const buildGameAchievementPath = (
|
||||||
|
game: { shop: GameShop; objectId: string; title: string },
|
||||||
|
user?: { userId: string; displayName: string }
|
||||||
|
) => {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
title: game.title,
|
||||||
|
shop: game.shop,
|
||||||
|
objectId: game.objectId,
|
||||||
|
userId: user?.userId || "",
|
||||||
|
displayName: user?.displayName || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
return `/achievements/?${searchParams.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||||
new Color(color).darken(amount).alpha(alpha).toString();
|
new Color(color).darken(amount).alpha(alpha).toString();
|
||||||
|
|
82
src/renderer/src/pages/achievement/achievements.css.ts
Normal file
82
src/renderer/src/pages/achievement/achievements.css.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
width: "100%",
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const header = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
width: "50%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const headerImage = style({
|
||||||
|
borderRadius: "4px",
|
||||||
|
objectFit: "cover",
|
||||||
|
cursor: "pointer",
|
||||||
|
height: "160px",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
":hover": {
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const list = style({
|
||||||
|
listStyle: "none",
|
||||||
|
margin: "0",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
padding: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listItem = style({
|
||||||
|
display: "flex",
|
||||||
|
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 * 2}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
textAlign: "left",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
textDecoration: "none",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listItemImage = recipe({
|
||||||
|
base: {
|
||||||
|
width: "54px",
|
||||||
|
height: "54px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
objectFit: "cover",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
unlocked: {
|
||||||
|
false: {
|
||||||
|
filter: "grayscale(100%)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const achievementsProgressBar = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "8px",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
"::-webkit-progress-bar": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
"::-webkit-progress-value": {
|
||||||
|
backgroundColor: vars.color.muted,
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,9 +1,17 @@
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { useAppDispatch, useDate } from "@renderer/hooks";
|
import { useAppDispatch, useDate } from "@renderer/hooks";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { GameAchievement, GameShop } from "@types";
|
import type { GameAchievement, GameShop } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
import * as styles from "./achievements.css";
|
||||||
|
import {
|
||||||
|
buildGameDetailsPath,
|
||||||
|
formatDownloadProgress,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
import { TrophyIcon } from "@primer/octicons-react";
|
||||||
|
import { vars } from "@renderer/theme.css";
|
||||||
|
|
||||||
export function Achievement() {
|
export function Achievement() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
@ -11,8 +19,12 @@ export function Achievement() {
|
||||||
const shop = searchParams.get("shop");
|
const shop = searchParams.get("shop");
|
||||||
const title = searchParams.get("title");
|
const title = searchParams.get("title");
|
||||||
const userId = searchParams.get("userId");
|
const userId = searchParams.get("userId");
|
||||||
|
const displayName = searchParams.get("displayName");
|
||||||
|
|
||||||
|
const { t } = useTranslation("achievement");
|
||||||
|
|
||||||
const { format } = useDate();
|
const { format } = useDate();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -30,53 +42,109 @@ export function Achievement() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (title) {
|
if (title) {
|
||||||
dispatch(setHeaderTitle(title + " Achievements"));
|
dispatch(setHeaderTitle(title));
|
||||||
}
|
}
|
||||||
}, [dispatch, title]);
|
}, [dispatch, title]);
|
||||||
|
|
||||||
return (
|
if (!objectId || !shop || !title) return null;
|
||||||
<div>
|
|
||||||
<h1>Achievement</h1>
|
|
||||||
|
|
||||||
<div
|
const unlockedAchievementCount = achievements.filter(
|
||||||
style={{
|
(achievement) => achievement.unlocked
|
||||||
display: "flex",
|
).length;
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
const totalAchievementCount = achievements.length;
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
const handleClickGame = () => {
|
||||||
>
|
navigate(
|
||||||
{achievements.map((achievement, index) => (
|
buildGameDetailsPath({
|
||||||
|
shop: shop as GameShop,
|
||||||
|
objectId,
|
||||||
|
title,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<button onClick={handleClickGame}>
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.cover(objectId)}
|
||||||
|
alt={title}
|
||||||
|
className={styles.headerImage}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
{displayName
|
||||||
|
? t("user_achievements", {
|
||||||
|
displayName,
|
||||||
|
})
|
||||||
|
: t("your_achievements")}
|
||||||
|
</h1>
|
||||||
<div
|
<div
|
||||||
key={index}
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
marginBottom: 8,
|
||||||
gap: `${SPACING_UNIT}px`,
|
color: vars.color.muted,
|
||||||
}}
|
}}
|
||||||
title={achievement.description}
|
|
||||||
>
|
>
|
||||||
<img
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: "60px",
|
display: "flex",
|
||||||
width: "60px",
|
alignItems: "center",
|
||||||
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
gap: 8,
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{unlockedAchievementCount} / {totalAchievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{formatDownloadProgress(
|
||||||
|
unlockedAchievementCount / totalAchievementCount
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={unlockedAchievementCount / totalAchievementCount}
|
||||||
|
className={styles.achievementsProgressBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{achievements.map((achievement, index) => (
|
||||||
|
<li key={index} className={styles.listItem}>
|
||||||
|
<img
|
||||||
|
className={styles.listItemImage({
|
||||||
|
unlocked: achievement.unlocked,
|
||||||
|
})}
|
||||||
src={
|
src={
|
||||||
achievement.unlocked ? achievement.icon : achievement.icongray
|
achievement.unlocked ? achievement.icon : achievement.icongray
|
||||||
}
|
}
|
||||||
alt={achievement.displayName}
|
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p>{achievement.displayName}</p>
|
<p>{achievement.displayName}</p>
|
||||||
<p>{achievement.description}</p>
|
<p>{achievement.description}</p>
|
||||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
<small>
|
||||||
|
{achievement.unlockTime && format(achievement.unlockTime)}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
||||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||||
|
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||||
|
@ -28,16 +29,6 @@ export function Sidebar() {
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const buildGameAchievementPath = () => {
|
|
||||||
const urlParams = new URLSearchParams({
|
|
||||||
objectId: objectId!,
|
|
||||||
shop,
|
|
||||||
title: gameTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
return `/achievements?${urlParams.toString()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (objectId) {
|
if (objectId) {
|
||||||
setHowLongToBeat({ isLoading: true, data: null });
|
setHowLongToBeat({ isLoading: true, data: null });
|
||||||
|
@ -88,7 +79,11 @@ export function Sidebar() {
|
||||||
{achievements.slice(0, 4).map((achievement, index) => (
|
{achievements.slice(0, 4).map((achievement, index) => (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
<Link
|
<Link
|
||||||
to={buildGameAchievementPath()}
|
to={buildGameAchievementPath({
|
||||||
|
shop: shop,
|
||||||
|
objectId: objectId!,
|
||||||
|
title: gameTitle,
|
||||||
|
})}
|
||||||
className={styles.listItem}
|
className={styles.listItem}
|
||||||
title={achievement.description}
|
title={achievement.description}
|
||||||
>
|
>
|
||||||
|
@ -116,7 +111,11 @@ export function Sidebar() {
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
style={{ textAlign: "center" }}
|
style={{ textAlign: "center" }}
|
||||||
to={buildGameAchievementPath()}
|
to={buildGameAchievementPath({
|
||||||
|
shop: shop,
|
||||||
|
objectId: objectId!,
|
||||||
|
title: gameTitle,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{t("see_all_achievements")}
|
{t("see_all_achievements")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { FriendsBox } from "./friends-box";
|
||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import { UserGame } from "@types";
|
import { UserGame } from "@types";
|
||||||
import {
|
import {
|
||||||
buildGameDetailsPath,
|
buildGameAchievementPath,
|
||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
} 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";
|
||||||
|
@ -44,11 +44,19 @@ export function ProfileContent() {
|
||||||
return userProfile?.relation?.status === "ACCEPTED";
|
return userProfile?.relation?.status === "ACCEPTED";
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
|
|
||||||
const buildUserGameDetailsPath = (game: UserGame) =>
|
const buildUserGameDetailsPath = (game: UserGame) => {
|
||||||
buildGameDetailsPath({
|
// TODO: check if user has hydra cloud
|
||||||
...game,
|
// buildGameDetailsPath({
|
||||||
objectId: game.objectId,
|
// ...game,
|
||||||
});
|
// objectId: game.objectId,
|
||||||
|
// });
|
||||||
|
|
||||||
|
const userParams = userProfile
|
||||||
|
? { userId: userProfile.id, displayName: userProfile.displayName }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return buildGameAchievementPath({ ...game }, userParams);
|
||||||
|
};
|
||||||
|
|
||||||
const formatPlayTime = useCallback(
|
const formatPlayTime = useCallback(
|
||||||
(playTimeInSeconds = 0) => {
|
(playTimeInSeconds = 0) => {
|
||||||
|
|
Loading…
Reference in a new issue