feat: pinning and showing featuring games in profile

This commit is contained in:
Moyasee
2025-09-23 15:21:32 +03:00
parent 2604dfea22
commit 33c15baf0e
15 changed files with 192 additions and 3 deletions

View File

@@ -126,6 +126,11 @@ declare global {
shop: GameShop,
objectId: string
) => Promise<void>;
addGameToPinned: (shop: GameShop, objectId: string) => Promise<void>;
removeGameFromPinned: (
shop: GameShop,
objectId: string
) => Promise<void>;
updateLaunchOptions: (
shop: GameShop,
objectId: string,

View File

@@ -3,6 +3,8 @@ import {
GearIcon,
HeartFillIcon,
HeartIcon,
PinIcon,
PinSlashIcon,
PlayIcon,
PlusCircleIcon,
} from "@primer/octicons-react";
@@ -82,6 +84,31 @@ export function HeroPanelActions() {
}
};
const toggleGamePinned = async () => {
setToggleLibraryGameDisabled(true);
try {
if (game?.pinned && objectId) {
await window.electron
.removeGameFromPinned(shop, objectId)
.then(() => {
showSuccessToast(t("game_removed_from_pinned"));
});
} else {
if (!objectId) return;
await window.electron.addGameToPinned(shop, objectId).then(() => {
showSuccessToast(t("game_added_to_pinned"));
});
}
updateLibrary();
updateGame();
} finally {
setToggleLibraryGameDisabled(false);
}
};
const openGame = async () => {
if (game) {
if (game.executablePath) {
@@ -198,6 +225,15 @@ export function HeroPanelActions() {
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
</Button>
<Button
onClick={toggleGamePinned}
theme="outline"
disabled={deleting}
className="hero-panel-actions__action"
>
{game.pinned ? <PinSlashIcon /> : <PinIcon />}
</Button>
<Button
onClick={() => setShowGameOptionsModal(true)}
theme="outline"

View File

@@ -58,6 +58,34 @@
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__tabs {
display: flex;
gap: calc(globals.$spacing-unit);
margin-bottom: calc(globals.$spacing-unit * 2);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
&__tab {
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
padding: calc(globals.$spacing-unit) calc(globals.$spacing-unit * 2);
cursor: pointer;
font-size: 14px;
font-weight: 500;
border-bottom: 2px solid transparent;
transition: all ease 0.2s;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
&--active {
color: white;
border-bottom-color: #c9aa71;
}
}
&__games-grid {
list-style: none;
margin: 0;

View File

@@ -68,6 +68,14 @@ export function ProfileContent() {
return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]);
const pinnedGames = useMemo(() => {
return userProfile?.libraryGames?.filter((game) => game.isPinned) || [];
}, [userProfile]);
const libraryGames = useMemo(() => {
return userProfile?.libraryGames || [];
}, [userProfile]);
const content = useMemo(() => {
if (!userProfile) return null;
@@ -80,6 +88,7 @@ export function ProfileContent() {
}
const hasGames = userProfile?.libraryGames.length > 0;
const hasPinnedGames = pinnedGames.length > 0;
const shouldShowRightContent = hasGames || userProfile.friends.length > 0;
@@ -98,16 +107,36 @@ export function ProfileContent() {
{hasGames && (
<>
{hasPinnedGames && (
<div style={{ marginBottom: '2rem' }}>
<div className="profile-content__section-header">
<h2>{t("pinned")}</h2>
<span>{pinnedGames.length}</span>
</div>
<ul className="profile-content__games-grid">
{pinnedGames?.map((game) => (
<UserLibraryGameCard
game={game}
key={game.objectId}
statIndex={statsIndex}
onMouseEnter={handleOnMouseEnterGameCard}
onMouseLeave={handleOnMouseLeaveGameCard}
/>
))}
</ul>
</div>
)}
<div className="profile-content__section-header">
<h2>{t("library")}</h2>
{userStats && (
<span>{numberFormatter.format(userStats.libraryCount)}</span>
)}
</div>
<ul className="profile-content__games-grid">
{userProfile?.libraryGames?.map((game) => (
{libraryGames?.map((game) => (
<UserLibraryGameCard
game={game}
key={game.objectId}

View File

@@ -65,6 +65,20 @@
padding: 8px;
}
&__favorite-icon {
position: absolute;
top: 8px;
right: 8px;
color: #ff6b6b;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 50%;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
&__playtime {
background-color: globals.$background-color;
color: globals.$muted-color;

View File

@@ -9,7 +9,7 @@ import {
formatDownloadProgress,
} from "@renderer/helpers";
import { userProfileContext } from "@renderer/context";
import { ClockIcon, TrophyIcon, AlertFillIcon } from "@primer/octicons-react";
import { ClockIcon, TrophyIcon, AlertFillIcon, HeartFillIcon } from "@primer/octicons-react";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next";
@@ -98,6 +98,11 @@ export function UserLibraryGameCard({
onClick={() => navigate(buildUserGameDetailsPath(game))}
>
<div className="user-library-game__overlay">
{game.isFavorite && (
<div className="user-library-game__favorite-icon">
<HeartFillIcon size={14} />
</div>
)}
<small
className="user-library-game__playtime"
data-tooltip-place="top"