mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 01:03:57 +00:00
feat: adding profile sorting
This commit is contained in:
@@ -454,6 +454,9 @@
|
||||
"activity": "Recent Activity",
|
||||
"library": "Library",
|
||||
"pinned": "Pinned",
|
||||
"achievements_earned": "Achievements earned",
|
||||
"played_recently": "Played recently",
|
||||
"playtime": "Playtime",
|
||||
"total_play_time": "Total playtime",
|
||||
"manual_playtime_tooltip": "This playtime has been manually updated",
|
||||
"no_recent_activity_title": "Hmmm… nothing here",
|
||||
@@ -530,7 +533,9 @@
|
||||
"show_achievements_on_profile": "Show your achievements on your profile",
|
||||
"show_points_on_profile": "Show your earned points on your profile",
|
||||
"error_adding_friend": "Could not send friend request. Please check friend code",
|
||||
"friend_code_length_error": "Friend code must have 8 characters"
|
||||
"friend_code_length_error": "Friend code must have 8 characters",
|
||||
"game_removed_from_pinned": "Game removed from pinned",
|
||||
"game_added_to_pinned": "Game added to pinned"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Achievement unlocked",
|
||||
|
||||
@@ -16,8 +16,7 @@ import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/add-game-to-favorites";
|
||||
import "./library/remove-game-from-favorites";
|
||||
import "./library/add-game-to-pinned";
|
||||
import "./library/remove-game-from-pinned";
|
||||
import "./library/toggle-game-pin";
|
||||
import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const addGameToPinned = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
const response = await HydraApi.put(`/profile/games/${shop}/${objectId}/pin`);
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
pinned: true,
|
||||
pinnedDate: new Date(response.pinnedDate),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update game pinned status: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("addGameToPinned", addGameToPinned);
|
||||
@@ -1,29 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const removeGameFromPinned = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`).catch(() => {});
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
pinned: false,
|
||||
pinnedDate: null,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update game pinned status: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("removeGameFromPinned", removeGameFromPinned);
|
||||
43
src/main/events/library/toggle-game-pin.ts
Normal file
43
src/main/events/library/toggle-game-pin.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import type { GameShop, UserGame } from "@types";
|
||||
|
||||
const toggleGamePin = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
pin: boolean
|
||||
) => {
|
||||
try {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
if (pin) {
|
||||
const response = await HydraApi.put<UserGame>(
|
||||
`/profile/games/${shop}/${objectId}/pin`
|
||||
);
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isPinned: pin,
|
||||
pinnedDate: new Date(response.pinnedDate!),
|
||||
});
|
||||
} else {
|
||||
await HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`);
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isPinned: pin,
|
||||
pinnedDate: null,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to update game pinned status", error);
|
||||
throw new Error(`Failed to update game pinned status: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("toggleGamePin", toggleGamePin);
|
||||
@@ -6,13 +6,18 @@ const getUserLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string,
|
||||
take: number = 12,
|
||||
skip: number = 0
|
||||
skip: number = 0,
|
||||
sortBy?: string
|
||||
): Promise<UserLibraryResponse | null> => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
params.append("take", take.toString());
|
||||
params.append("skip", skip.toString());
|
||||
|
||||
if (sortBy) {
|
||||
params.append("sortBy", sortBy);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const baseUrl = `/users/${userId}/library`;
|
||||
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
|
||||
@@ -37,7 +37,7 @@ export const mergeWithRemoteGames = async () => {
|
||||
lastTimePlayed: updatedLastTimePlayed,
|
||||
playTimeInMilliseconds: updatedPlayTime,
|
||||
favorite: game.isFavorite ?? localGame.favorite,
|
||||
pinned: game.isPinned ?? localGame.pinned,
|
||||
isPinned: game.isPinned ?? localGame.isPinned,
|
||||
});
|
||||
} else {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
@@ -51,7 +51,7 @@ export const mergeWithRemoteGames = async () => {
|
||||
hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime,
|
||||
isDeleted: false,
|
||||
favorite: game.isFavorite ?? false,
|
||||
pinned: game.isPinned ?? false,
|
||||
isPinned: game.isPinned ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export const uploadGamesBatch = async () => {
|
||||
shop: game.shop,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
isFavorite: game.favorite,
|
||||
isPinned: game.pinned ?? false,
|
||||
isPinned: game.isPinned ?? false,
|
||||
};
|
||||
})
|
||||
).catch(() => {});
|
||||
|
||||
@@ -143,10 +143,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||
addGameToPinned: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("addGameToPinned", shop, objectId),
|
||||
removeGameFromPinned: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("removeGameFromPinned", shop, objectId),
|
||||
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
|
||||
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
|
||||
updateLaunchOptions: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -370,8 +368,12 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* User */
|
||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||
getUserLibrary: (userId: string, take?: number, skip?: number) =>
|
||||
ipcRenderer.invoke("getUserLibrary", userId, take, skip),
|
||||
getUserLibrary: (
|
||||
userId: string,
|
||||
take?: number,
|
||||
skip?: number,
|
||||
sortBy?: string
|
||||
) => ipcRenderer.invoke("getUserLibrary", userId, take, skip, sortBy),
|
||||
blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
|
||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface UserProfileContext {
|
||||
isMe: boolean;
|
||||
userStats: UserStats | null;
|
||||
getUserProfile: () => Promise<void>;
|
||||
getUserLibraryGames: () => Promise<void>;
|
||||
getUserLibraryGames: (sortBy?: string) => Promise<void>;
|
||||
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
||||
backgroundImage: string;
|
||||
badges: Badge[];
|
||||
@@ -30,7 +30,7 @@ export const userProfileContext = createContext<UserProfileContext>({
|
||||
isMe: false,
|
||||
userStats: null,
|
||||
getUserProfile: async () => {},
|
||||
getUserLibraryGames: async () => {},
|
||||
getUserLibraryGames: async (_sortBy?: string) => {},
|
||||
setSelectedBackgroundImage: () => {},
|
||||
backgroundImage: "",
|
||||
badges: [],
|
||||
@@ -93,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();
|
||||
|
||||
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>;
|
||||
|
||||
@@ -94,14 +94,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"));
|
||||
});
|
||||
}
|
||||
@@ -236,7 +236,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);
|
||||
|
||||
@@ -65,8 +65,103 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__section-count {
|
||||
margin-left: auto;
|
||||
&__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;
|
||||
}
|
||||
|
||||
&__sort-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(globals.$spacing-unit);
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__sort-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__sort-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__sort-option {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
transition: all ease 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
color: rgba(201, 170, 113, 0.8);
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid rgba(201, 170, 113, 0.3);
|
||||
border-top: 2px solid rgba(201, 170, 113, 0.8);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: translateY(-50%) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__sort-separator {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__collapse-button {
|
||||
|
||||
@@ -3,8 +3,15 @@ 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, ChevronRightIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
TelescopeIcon,
|
||||
ChevronRightIcon,
|
||||
TrophyIcon,
|
||||
ClockIcon,
|
||||
HistoryIcon,
|
||||
} 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";
|
||||
@@ -59,6 +66,35 @@ const gameCardVariants = {
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
scale: 0.95,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const gameGridVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const chevronVariants = {
|
||||
@@ -78,11 +114,23 @@ const chevronVariants = {
|
||||
},
|
||||
};
|
||||
|
||||
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 [isLoadingSort, setIsLoadingSort] = useState(false);
|
||||
const [prevLibraryGames, setPrevLibraryGames] = useState<UserGame[]>([]);
|
||||
const [prevPinnedGames, setPrevPinnedGames] = useState<UserGame[]>([]);
|
||||
const statsAnimation = useRef(-1);
|
||||
const { toggleSection, isPinnedCollapsed } = useSectionCollapse();
|
||||
|
||||
@@ -98,6 +146,15 @@ export function ProfileContent() {
|
||||
}
|
||||
}, [userProfile, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile) {
|
||||
setIsLoadingSort(true);
|
||||
getUserLibraryGames(sortBy).finally(() => {
|
||||
setIsLoadingSort(false);
|
||||
});
|
||||
}
|
||||
}, [sortBy, getUserLibraryGames, userProfile]);
|
||||
|
||||
const handleOnMouseEnterGameCard = () => {
|
||||
setIsAnimationRunning(false);
|
||||
};
|
||||
@@ -129,10 +186,68 @@ export function ProfileContent() {
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
// Function to check if game lists have changed
|
||||
const gamesHaveChanged = (
|
||||
current: UserGame[],
|
||||
previous: UserGame[]
|
||||
): boolean => {
|
||||
if (current.length !== previous.length) return true;
|
||||
return current.some(
|
||||
(game, index) => game.objectId !== previous[index]?.objectId
|
||||
);
|
||||
};
|
||||
|
||||
// Check if animations should run
|
||||
const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames);
|
||||
const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames);
|
||||
|
||||
// Update previous games when lists change
|
||||
useEffect(() => {
|
||||
setPrevLibraryGames(libraryGames);
|
||||
}, [libraryGames]);
|
||||
|
||||
useEffect(() => {
|
||||
setPrevPinnedGames(pinnedGames);
|
||||
}, [pinnedGames]);
|
||||
|
||||
const usersAreFriends = useMemo(() => {
|
||||
return userProfile?.relation?.status === "ACCEPTED";
|
||||
}, [userProfile]);
|
||||
|
||||
const SortOptions = () => (
|
||||
<div className="profile-content__sort-container">
|
||||
<span className="profile-content__sort-label">Sort by:</span>
|
||||
<div className="profile-content__sort-options">
|
||||
<button
|
||||
className={`profile-content__sort-option ${sortBy === "achievementCount" ? "active" : ""} ${isLoadingSort && sortBy === "achievementCount" ? "loading" : ""}`}
|
||||
onClick={() => setSortBy("achievementCount")}
|
||||
disabled={isLoadingSort}
|
||||
>
|
||||
<TrophyIcon size={16} />
|
||||
<span>{t("achievements_earned")}</span>
|
||||
</button>
|
||||
<span className="profile-content__sort-separator">|</span>
|
||||
<button
|
||||
className={`profile-content__sort-option ${sortBy === "playedRecently" ? "active" : ""} ${isLoadingSort && sortBy === "playedRecently" ? "loading" : ""}`}
|
||||
onClick={() => setSortBy("playedRecently")}
|
||||
disabled={isLoadingSort}
|
||||
>
|
||||
<HistoryIcon size={16} />
|
||||
<span>{t("played_recently")}</span>
|
||||
</button>
|
||||
<span className="profile-content__sort-separator">|</span>
|
||||
<button
|
||||
className={`profile-content__sort-option ${sortBy === "playtime" ? "active" : ""} ${isLoadingSort && sortBy === "playtime" ? "loading" : ""}`}
|
||||
onClick={() => setSortBy("playtime")}
|
||||
disabled={isLoadingSort}
|
||||
>
|
||||
<ClockIcon size={16} />
|
||||
<span>{t("playtime")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (!userProfile) return null;
|
||||
|
||||
@@ -154,6 +269,8 @@ export function ProfileContent() {
|
||||
return (
|
||||
<section className="profile-content__section">
|
||||
<div className="profile-content__main">
|
||||
{hasAnyGames && <SortOptions />}
|
||||
|
||||
{!hasAnyGames && (
|
||||
<div className="profile-content__no-games">
|
||||
<div className="profile-content__telescope-icon">
|
||||
@@ -188,10 +305,10 @@ export function ProfileContent() {
|
||||
</motion.div>
|
||||
</button>
|
||||
<h2>{t("pinned")}</h2>
|
||||
<span className="profile-content__section-badge">
|
||||
{pinnedGames.length}
|
||||
</span>
|
||||
</div>
|
||||
<span className="profile-content__section-count">
|
||||
{pinnedGames.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={true} mode="wait">
|
||||
@@ -204,25 +321,57 @@ export function ProfileContent() {
|
||||
exit="collapsed"
|
||||
layout
|
||||
>
|
||||
<ul className="profile-content__games-grid">
|
||||
{pinnedGames?.map((game, index) => (
|
||||
<motion.li
|
||||
key={game.objectId}
|
||||
variants={gameCardVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: index * 0.1 }}
|
||||
style={{ listStyle: "none" }}
|
||||
>
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
<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>
|
||||
@@ -234,33 +383,62 @@ export function ProfileContent() {
|
||||
<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>
|
||||
{userStats && (
|
||||
<span className="profile-content__section-count">
|
||||
{numberFormatter.format(userStats.libraryCount)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="profile-content__games-grid">
|
||||
{libraryGames?.map((game, index) => (
|
||||
<motion.li
|
||||
key={game.objectId}
|
||||
variants={gameCardVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: index * 0.1 }}
|
||||
style={{ listStyle: "none" }}
|
||||
>
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
statIndex={statsIndex}
|
||||
onMouseEnter={handleOnMouseEnterGameCard}
|
||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||
/>
|
||||
</motion.li>
|
||||
))}
|
||||
</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>
|
||||
|
||||
@@ -38,7 +38,6 @@ export function UserLibraryGameCard({
|
||||
const { userProfile, isMe, getUserLibraryGames } =
|
||||
useContext(userProfileContext);
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { t: tGame } = useTranslation("game_details");
|
||||
const { numberFormatter } = useFormat();
|
||||
const { showSuccessToast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
@@ -99,23 +98,19 @@ export function UserLibraryGameCard({
|
||||
setIsPinning(true);
|
||||
|
||||
try {
|
||||
if (game.isPinned) {
|
||||
await window.electron
|
||||
.removeGameFromPinned(game.shop, game.objectId)
|
||||
.then(() => {
|
||||
showSuccessToast(tGame("game_removed_from_pinned"));
|
||||
});
|
||||
} else {
|
||||
await window.electron
|
||||
.addGameToPinned(game.shop, game.objectId)
|
||||
.then(() => {
|
||||
showSuccessToast(tGame("game_added_to_pinned"));
|
||||
});
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface Game {
|
||||
executablePath?: string | null;
|
||||
launchOptions?: string | null;
|
||||
favorite?: boolean;
|
||||
pinned?: boolean;
|
||||
isPinned?: boolean;
|
||||
pinnedDate?: Date | null;
|
||||
automaticCloudSync?: boolean;
|
||||
hasManuallyUpdatedPlaytime?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user