mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-22 10:23:56 +00:00
Merge branch 'main' into feat/context_menu-and-enhancement-actions
This commit is contained in:
@@ -14,6 +14,7 @@ export interface UserProfileContext {
|
||||
isMe: boolean;
|
||||
userStats: UserStats | null;
|
||||
getUserProfile: () => Promise<void>;
|
||||
getUserLibraryGames: (sortBy?: string) => Promise<void>;
|
||||
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
||||
backgroundImage: string;
|
||||
badges: Badge[];
|
||||
@@ -29,6 +30,7 @@ export const userProfileContext = createContext<UserProfileContext>({
|
||||
isMe: false,
|
||||
userStats: null,
|
||||
getUserProfile: async () => {},
|
||||
getUserLibraryGames: async (_sortBy?: string) => {},
|
||||
setSelectedBackgroundImage: () => {},
|
||||
backgroundImage: "",
|
||||
badges: [],
|
||||
@@ -91,21 +93,30 @@ export function UserProfileContextProvider({
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
const getUserLibraryGames = useCallback(async () => {
|
||||
try {
|
||||
const response = await window.electron.getUserLibrary(userId);
|
||||
if (response) {
|
||||
setLibraryGames(response.library);
|
||||
setPinnedGames(response.pinnedGames);
|
||||
} else {
|
||||
const getUserLibraryGames = useCallback(
|
||||
async (sortBy?: string) => {
|
||||
try {
|
||||
const response = await window.electron.getUserLibrary(
|
||||
userId,
|
||||
12,
|
||||
0,
|
||||
sortBy
|
||||
);
|
||||
|
||||
if (response) {
|
||||
setLibraryGames(response.library);
|
||||
setPinnedGames(response.pinnedGames);
|
||||
} else {
|
||||
setLibraryGames([]);
|
||||
setPinnedGames([]);
|
||||
}
|
||||
} catch (error) {
|
||||
setLibraryGames([]);
|
||||
setPinnedGames([]);
|
||||
}
|
||||
} catch (error) {
|
||||
setLibraryGames([]);
|
||||
setPinnedGames([]);
|
||||
}
|
||||
}, [userId]);
|
||||
},
|
||||
[userId]
|
||||
);
|
||||
|
||||
const getUserProfile = useCallback(async () => {
|
||||
getUserStats();
|
||||
@@ -149,6 +160,7 @@ export function UserProfileContextProvider({
|
||||
heroBackground,
|
||||
isMe,
|
||||
getUserProfile,
|
||||
getUserLibraryGames,
|
||||
setSelectedBackgroundImage,
|
||||
backgroundImage: getBackgroundImageUrl(),
|
||||
userStats,
|
||||
|
||||
10
src/renderer/src/declaration.d.ts
vendored
10
src/renderer/src/declaration.d.ts
vendored
@@ -127,8 +127,11 @@ declare global {
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<void>;
|
||||
addGameToPinned: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
removeGameFromPinned: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
toggleGamePin: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
pinned: boolean
|
||||
) => Promise<void>;
|
||||
updateLaunchOptions: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -293,7 +296,8 @@ declare global {
|
||||
getUserLibrary: (
|
||||
userId: string,
|
||||
take?: number,
|
||||
skip?: number
|
||||
skip?: number,
|
||||
sortBy?: string
|
||||
) => Promise<UserLibraryResponse>;
|
||||
blockUser: (userId: string) => Promise<void>;
|
||||
unblockUser: (userId: string) => Promise<void>;
|
||||
|
||||
27
src/renderer/src/hooks/use-section-collapse.ts
Normal file
27
src/renderer/src/hooks/use-section-collapse.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
interface SectionCollapseState {
|
||||
pinned: boolean;
|
||||
library: boolean;
|
||||
}
|
||||
|
||||
export function useSectionCollapse() {
|
||||
const [collapseState, setCollapseState] = useState<SectionCollapseState>({
|
||||
pinned: false,
|
||||
library: false,
|
||||
});
|
||||
|
||||
const toggleSection = useCallback((section: keyof SectionCollapseState) => {
|
||||
setCollapseState((prevState) => ({
|
||||
...prevState,
|
||||
[section]: !prevState[section],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
collapseState,
|
||||
toggleSection,
|
||||
isPinnedCollapsed: collapseState.pinned,
|
||||
isLibraryCollapsed: collapseState.library,
|
||||
};
|
||||
}
|
||||
@@ -122,14 +122,14 @@ export function HeroPanelActions() {
|
||||
setToggleLibraryGameDisabled(true);
|
||||
|
||||
try {
|
||||
if (game?.pinned && objectId) {
|
||||
await window.electron.removeGameFromPinned(shop, objectId).then(() => {
|
||||
if (game?.isPinned && objectId) {
|
||||
await window.electron.toggleGamePin(shop, objectId, false).then(() => {
|
||||
showSuccessToast(t("game_removed_from_pinned"));
|
||||
});
|
||||
} else {
|
||||
if (!objectId) return;
|
||||
|
||||
await window.electron.addGameToPinned(shop, objectId).then(() => {
|
||||
await window.electron.toggleGamePin(shop, objectId, true).then(() => {
|
||||
showSuccessToast(t("game_added_to_pinned"));
|
||||
});
|
||||
}
|
||||
@@ -264,7 +264,7 @@ export function HeroPanelActions() {
|
||||
disabled={deleting}
|
||||
className="hero-panel-actions__action"
|
||||
>
|
||||
{game.pinned ? <PinSlashIcon /> : <PinIcon />}
|
||||
{game.isPinned ? <PinSlashIcon /> : <PinIcon />}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ export function ChangeGamePlaytimeModal({
|
||||
onSuccess?.(t("update_playtime_success"));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
onError?.(t("update_playtime_error"));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
||||
@@ -54,8 +54,47 @@
|
||||
&__section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
gap: calc(globals.$spacing-unit);
|
||||
}
|
||||
|
||||
&__section-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__section-badge {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
&__collapse-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all ease 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
|
||||
@@ -3,24 +3,45 @@ 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 { TelescopeIcon } from "@primer/octicons-react";
|
||||
import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserGame } from "@types";
|
||||
import { LockedProfile } from "./locked-profile";
|
||||
import { ReportProfile } from "../report-profile/report-profile";
|
||||
import { FriendsBox } from "./friends-box";
|
||||
import { RecentGamesBox } from "./recent-games-box";
|
||||
import { UserStatsBox } from "./user-stats-box";
|
||||
import { UserLibraryGameCard } from "./user-library-game-card";
|
||||
import { SortOptions } from "./sort-options";
|
||||
import { useSectionCollapse } from "@renderer/hooks/use-section-collapse";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
sectionVariants,
|
||||
gameCardVariants,
|
||||
gameGridVariants,
|
||||
chevronVariants,
|
||||
GAME_STATS_ANIMATION_DURATION_IN_MS,
|
||||
} from "./profile-animations";
|
||||
import "./profile-content.scss";
|
||||
|
||||
const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
||||
type SortOption = "playtime" | "achievementCount" | "playedRecently";
|
||||
|
||||
export function ProfileContent() {
|
||||
const { userProfile, isMe, userStats, libraryGames, pinnedGames } =
|
||||
useContext(userProfileContext);
|
||||
const {
|
||||
userProfile,
|
||||
isMe,
|
||||
userStats,
|
||||
libraryGames,
|
||||
pinnedGames,
|
||||
getUserLibraryGames,
|
||||
} = useContext(userProfileContext);
|
||||
const [statsIndex, setStatsIndex] = useState(0);
|
||||
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
||||
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
|
||||
const [prevLibraryGames, setPrevLibraryGames] = useState<UserGame[]>([]);
|
||||
const [prevPinnedGames, setPrevPinnedGames] = useState<UserGame[]>([]);
|
||||
const statsAnimation = useRef(-1);
|
||||
const { toggleSection, isPinnedCollapsed } = useSectionCollapse();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -34,6 +55,12 @@ export function ProfileContent() {
|
||||
}
|
||||
}, [userProfile, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile) {
|
||||
getUserLibraryGames(sortBy);
|
||||
}
|
||||
}, [sortBy, getUserLibraryGames, userProfile]);
|
||||
|
||||
const handleOnMouseEnterGameCard = () => {
|
||||
setIsAnimationRunning(false);
|
||||
};
|
||||
@@ -65,6 +92,27 @@ export function ProfileContent() {
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const gamesHaveChanged = (
|
||||
current: UserGame[],
|
||||
previous: UserGame[]
|
||||
): boolean => {
|
||||
if (current.length !== previous.length) return true;
|
||||
return current.some(
|
||||
(game, index) => game.objectId !== previous[index]?.objectId
|
||||
);
|
||||
};
|
||||
|
||||
const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames);
|
||||
const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames);
|
||||
|
||||
useEffect(() => {
|
||||
setPrevLibraryGames(libraryGames);
|
||||
}, [libraryGames]);
|
||||
|
||||
useEffect(() => {
|
||||
setPrevPinnedGames(pinnedGames);
|
||||
}, [pinnedGames]);
|
||||
|
||||
const usersAreFriends = useMemo(() => {
|
||||
return userProfile?.relation?.status === "ACCEPTED";
|
||||
}, [userProfile]);
|
||||
@@ -82,13 +130,19 @@ export function ProfileContent() {
|
||||
|
||||
const hasGames = libraryGames.length > 0;
|
||||
const hasPinnedGames = pinnedGames.length > 0;
|
||||
const hasAnyGames = hasGames || hasPinnedGames;
|
||||
|
||||
const shouldShowRightContent = hasGames || userProfile.friends.length > 0;
|
||||
const shouldShowRightContent =
|
||||
hasAnyGames || userProfile.friends.length > 0;
|
||||
|
||||
return (
|
||||
<section className="profile-content__section">
|
||||
<div className="profile-content__main">
|
||||
{!hasGames && (
|
||||
{hasAnyGames && (
|
||||
<SortOptions sortBy={sortBy} onSortChange={setSortBy} />
|
||||
)}
|
||||
|
||||
{!hasAnyGames && (
|
||||
<div className="profile-content__no-games">
|
||||
<div className="profile-content__telescope-icon">
|
||||
<TelescopeIcon size={24} />
|
||||
@@ -98,48 +152,167 @@ export function ProfileContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasGames && (
|
||||
<>
|
||||
{hasAnyGames && (
|
||||
<div>
|
||||
{hasPinnedGames && (
|
||||
<div style={{ marginBottom: "2rem" }}>
|
||||
<div className="profile-content__section-header">
|
||||
<h2>{t("pinned")}</h2>
|
||||
<span>{pinnedGames.length}</span>
|
||||
<div className="profile-content__section-title-group">
|
||||
<button
|
||||
type="button"
|
||||
className="profile-content__collapse-button"
|
||||
onClick={() => toggleSection("pinned")}
|
||||
aria-label={
|
||||
isPinnedCollapsed
|
||||
? "Expand pinned section"
|
||||
: "Collapse pinned section"
|
||||
}
|
||||
>
|
||||
<motion.div
|
||||
variants={chevronVariants}
|
||||
animate={isPinnedCollapsed ? "collapsed" : "expanded"}
|
||||
>
|
||||
<ChevronRightIcon size={16} />
|
||||
</motion.div>
|
||||
</button>
|
||||
<h2>{t("pinned")}</h2>
|
||||
<span className="profile-content__section-badge">
|
||||
{pinnedGames.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="profile-content__games-grid">
|
||||
{pinnedGames?.map((game) => (
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
key={game.objectId}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<AnimatePresence initial={true} mode="wait">
|
||||
{!isPinnedCollapsed && (
|
||||
<motion.div
|
||||
key="pinned-content"
|
||||
variants={sectionVariants}
|
||||
initial="collapsed"
|
||||
animate="expanded"
|
||||
exit="collapsed"
|
||||
layout
|
||||
>
|
||||
<motion.ul
|
||||
className="profile-content__games-grid"
|
||||
variants={
|
||||
shouldAnimatePinned ? gameGridVariants : undefined
|
||||
}
|
||||
initial={shouldAnimatePinned ? "hidden" : undefined}
|
||||
animate={shouldAnimatePinned ? "visible" : undefined}
|
||||
exit={shouldAnimatePinned ? "exit" : undefined}
|
||||
key={
|
||||
shouldAnimatePinned
|
||||
? `pinned-${sortBy}`
|
||||
: `pinned-static`
|
||||
}
|
||||
>
|
||||
{shouldAnimatePinned ? (
|
||||
<AnimatePresence mode="wait">
|
||||
{pinnedGames?.map((game, index) => (
|
||||
<motion.li
|
||||
key={game.objectId}
|
||||
variants={gameCardVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
transition={{ delay: index * 0.1 }}
|
||||
style={{ listStyle: "none" }}
|
||||
>
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
</motion.li>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
) : (
|
||||
pinnedGames?.map((game) => (
|
||||
<li
|
||||
key={game.objectId}
|
||||
style={{ listStyle: "none" }}
|
||||
>
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="profile-content__section-header">
|
||||
<h2>{t("library")}</h2>
|
||||
{userStats && (
|
||||
<span>{numberFormatter.format(userStats.libraryCount)}</span>
|
||||
)}
|
||||
</div>
|
||||
{hasGames && (
|
||||
<div>
|
||||
<div className="profile-content__section-header">
|
||||
<div className="profile-content__section-title-group">
|
||||
<h2>{t("library")}</h2>
|
||||
{userStats && (
|
||||
<span className="profile-content__section-badge">
|
||||
{numberFormatter.format(userStats.libraryCount)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="profile-content__games-grid">
|
||||
{libraryGames?.map((game) => (
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
key={game.objectId}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
<motion.ul
|
||||
className="profile-content__games-grid"
|
||||
variants={
|
||||
shouldAnimateLibrary ? gameGridVariants : undefined
|
||||
}
|
||||
initial={shouldAnimateLibrary ? "hidden" : undefined}
|
||||
animate={shouldAnimateLibrary ? "visible" : undefined}
|
||||
exit={shouldAnimateLibrary ? "exit" : undefined}
|
||||
key={
|
||||
shouldAnimateLibrary
|
||||
? `library-${sortBy}`
|
||||
: `library-static`
|
||||
}
|
||||
>
|
||||
{shouldAnimateLibrary ? (
|
||||
<AnimatePresence mode="wait">
|
||||
{libraryGames?.map((game, index) => (
|
||||
<motion.li
|
||||
key={game.objectId}
|
||||
variants={gameCardVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
transition={{ delay: index * 0.1 }}
|
||||
style={{ listStyle: "none" }}
|
||||
>
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
</motion.li>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
) : (
|
||||
libraryGames?.map((game) => (
|
||||
<li key={game.objectId} style={{ listStyle: "none" }}>
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</motion.ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -163,6 +336,11 @@ export function ProfileContent() {
|
||||
statsIndex,
|
||||
libraryGames,
|
||||
pinnedGames,
|
||||
isPinnedCollapsed,
|
||||
toggleSection,
|
||||
shouldAnimateLibrary,
|
||||
shouldAnimatePinned,
|
||||
sortBy,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -65,18 +65,45 @@
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&__favorite-icon {
|
||||
&__actions-container {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
color: #ff6b6b;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&__favorite-icon {
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&__pin-button {
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__playtime {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UserGame } from "@types";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import { useFormat, useToast } from "@renderer/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCallback, useContext, useState } from "react";
|
||||
import {
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
TrophyIcon,
|
||||
AlertFillIcon,
|
||||
HeartFillIcon,
|
||||
PinIcon,
|
||||
PinSlashIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
@@ -33,11 +35,14 @@ export function UserLibraryGameCard({
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: UserLibraryGameCardProps) {
|
||||
const { userProfile } = useContext(userProfileContext);
|
||||
const { userProfile, isMe, getUserLibraryGames } =
|
||||
useContext(userProfileContext);
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { numberFormatter } = useFormat();
|
||||
const { showSuccessToast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [isTooltipHovered, setIsTooltipHovered] = useState(false);
|
||||
const [isPinning, setIsPinning] = useState(false);
|
||||
|
||||
const getStatsItemCount = useCallback(() => {
|
||||
let statsCount = 1;
|
||||
@@ -89,6 +94,28 @@ export function UserLibraryGameCard({
|
||||
[numberFormatter, t]
|
||||
);
|
||||
|
||||
const toggleGamePinned = async () => {
|
||||
setIsPinning(true);
|
||||
|
||||
try {
|
||||
await window.electron.toggleGamePin(
|
||||
game.shop,
|
||||
game.objectId,
|
||||
!game.isPinned
|
||||
);
|
||||
|
||||
await getUserLibraryGames();
|
||||
|
||||
if (game.isPinned) {
|
||||
showSuccessToast(t("game_removed_from_pinned"));
|
||||
} else {
|
||||
showSuccessToast(t("game_added_to_pinned"));
|
||||
}
|
||||
} finally {
|
||||
setIsPinning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<li
|
||||
@@ -103,9 +130,30 @@ 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} />
|
||||
{(game.isFavorite || isMe) && (
|
||||
<div className="user-library-game__actions-container">
|
||||
{game.isFavorite && (
|
||||
<div className="user-library-game__favorite-icon">
|
||||
<HeartFillIcon size={12} />
|
||||
</div>
|
||||
)}
|
||||
{isMe && (
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__pin-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleGamePinned();
|
||||
}}
|
||||
disabled={isPinning}
|
||||
>
|
||||
{game.isPinned ? (
|
||||
<PinSlashIcon size={12} />
|
||||
) : (
|
||||
<PinIcon size={12} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<small
|
||||
|
||||
Reference in New Issue
Block a user