diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 733f63d7..00b387d2 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -66,6 +66,7 @@ import "./auth/sign-out"; import "./auth/open-auth-window"; import "./auth/get-session-hash"; import "./user/get-user"; +import "./user/get-user-library"; import "./user/get-blocked-users"; import "./user/block-user"; import "./user/unblock-user"; diff --git a/src/main/events/library/add-game-to-pinned.ts b/src/main/events/library/add-game-to-pinned.ts index 8ed5921f..95ea0b82 100644 --- a/src/main/events/library/add-game-to-pinned.ts +++ b/src/main/events/library/add-game-to-pinned.ts @@ -19,10 +19,11 @@ const addGameToPinned = async ( await gamesSublevel.put(gameKey, { ...game, pinned: true, + pinnedDate: new Date(), }); } catch (error) { throw new Error(`Failed to update game pinned status: ${error}`); } }; -registerEvent("addGameToPinned", addGameToPinned); \ No newline at end of file +registerEvent("addGameToPinned", addGameToPinned); diff --git a/src/main/events/library/remove-game-from-pinned.ts b/src/main/events/library/remove-game-from-pinned.ts index 613284bd..658b9d6d 100644 --- a/src/main/events/library/remove-game-from-pinned.ts +++ b/src/main/events/library/remove-game-from-pinned.ts @@ -19,10 +19,11 @@ const removeGameFromPinned = async ( await gamesSublevel.put(gameKey, { ...game, pinned: false, + pinnedDate: null, }); } catch (error) { throw new Error(`Failed to update game pinned status: ${error}`); } }; -registerEvent("removeGameFromPinned", removeGameFromPinned); \ No newline at end of file +registerEvent("removeGameFromPinned", removeGameFromPinned); diff --git a/src/main/events/user/get-user-library.ts b/src/main/events/user/get-user-library.ts new file mode 100644 index 00000000..b7d6a86d --- /dev/null +++ b/src/main/events/user/get-user-library.ts @@ -0,0 +1,27 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import type { UserLibraryResponse } from "@types"; + +const getUserLibrary = async ( + _event: Electron.IpcMainInvokeEvent, + userId: string, + take?: number, + skip?: number +): Promise => { + const params = new URLSearchParams(); + + if (take !== undefined) { + params.append('take', take.toString()); + } + + if (skip !== undefined) { + params.append('skip', skip.toString()); + } + + const queryString = params.toString(); + const url = `/users/${userId}/library${queryString ? `?${queryString}` : ''}`; + + return HydraApi.get(url).catch(() => null); +}; + +registerEvent("getUserLibrary", getUserLibrary); \ No newline at end of file diff --git a/src/preload/index.ts b/src/preload/index.ts index b3c4f400..608d8dfb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -370,6 +370,7 @@ contextBridge.exposeInMainWorld("electron", { /* User */ getUser: (userId: string) => ipcRenderer.invoke("getUser", userId), + getUserLibrary: (userId: string, take?: number, skip?: number) => ipcRenderer.invoke("getUserLibrary", userId, take, skip), blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId), unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId), getUserFriends: (userId: string, take: number, skip: number) => diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx index 9b9d16b4..e7cbdfd9 100644 --- a/src/renderer/src/context/user-profile/user-profile.context.tsx +++ b/src/renderer/src/context/user-profile/user-profile.context.tsx @@ -1,6 +1,6 @@ import { darkenColor } from "@renderer/helpers"; import { useAppSelector, useToast } from "@renderer/hooks"; -import type { Badge, UserProfile, UserStats } from "@types"; +import type { Badge, UserProfile, UserStats, UserGame } from "@types"; import { average } from "color.js"; import { createContext, useCallback, useEffect, useState } from "react"; @@ -17,6 +17,8 @@ export interface UserProfileContext { setSelectedBackgroundImage: React.Dispatch>; backgroundImage: string; badges: Badge[]; + libraryGames: UserGame[]; + pinnedGames: UserGame[]; } export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3"; @@ -30,6 +32,8 @@ export const userProfileContext = createContext({ setSelectedBackgroundImage: () => {}, backgroundImage: "", badges: [], + libraryGames: [], + pinnedGames: [], }); const { Provider } = userProfileContext; @@ -49,6 +53,8 @@ export function UserProfileContextProvider({ const [userStats, setUserStats] = useState(null); const [userProfile, setUserProfile] = useState(null); + const [libraryGames, setLibraryGames] = useState([]); + const [pinnedGames, setPinnedGames] = useState([]); const [badges, setBadges] = useState([]); const [heroBackground, setHeroBackground] = useState( DEFAULT_USER_PROFILE_BACKGROUND @@ -85,8 +91,27 @@ export function UserProfileContextProvider({ }); }, [userId]); + const getUserLibraryGames = useCallback(async () => { + try { + // Example usage with pagination: take=24, skip=0 + const response = await window.electron.getUserLibrary(userId, 24, 0); + if (response) { + setLibraryGames(response.library); + setPinnedGames(response.pinnedGames); + } else { + setLibraryGames([]); + setPinnedGames([]); + } + } catch (error) { + console.error("Failed to fetch user library games:", error); + setLibraryGames([]); + setPinnedGames([]); + } + }, [userId]); + const getUserProfile = useCallback(async () => { getUserStats(); + getUserLibraryGames(); return window.electron.getUser(userId).then((userProfile) => { if (userProfile) { @@ -102,7 +127,7 @@ export function UserProfileContextProvider({ navigate(-1); } }); - }, [navigate, getUserStats, showErrorToast, userId, t]); + }, [navigate, getUserStats, getUserLibraryGames, showErrorToast, userId, t]); const getBadges = useCallback(async () => { const badges = await window.electron.getBadges(); @@ -111,6 +136,8 @@ export function UserProfileContextProvider({ useEffect(() => { setUserProfile(null); + setLibraryGames([]); + setPinnedGames([]); setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND); getUserProfile(); @@ -128,6 +155,8 @@ export function UserProfileContextProvider({ backgroundImage: getBackgroundImageUrl(), userStats, badges, + libraryGames, + pinnedGames, }} > {children} diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 434c0adb..c81a7e86 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -37,6 +37,8 @@ import type { ShopDetailsWithAssets, AchievementCustomNotificationPosition, AchievementNotificationInfo, + UserGame, + UserLibraryResponse, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type disk from "diskusage"; @@ -127,10 +129,7 @@ declare global { objectId: string ) => Promise; addGameToPinned: (shop: GameShop, objectId: string) => Promise; - removeGameFromPinned: ( - shop: GameShop, - objectId: string - ) => Promise; + removeGameFromPinned: (shop: GameShop, objectId: string) => Promise; updateLaunchOptions: ( shop: GameShop, objectId: string, @@ -292,6 +291,7 @@ declare global { /* User */ getUser: (userId: string) => Promise; + getUserLibrary: (userId: string, take?: number, skip?: number) => Promise; blockUser: (userId: string) => Promise; unblockUser: (userId: string) => Promise; getUserFriends: ( diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index bb3dfe98..134f1d49 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -89,11 +89,9 @@ export function HeroPanelActions() { try { if (game?.pinned && objectId) { - await window.electron - .removeGameFromPinned(shop, objectId) - .then(() => { - showSuccessToast(t("game_removed_from_pinned")); - }); + await window.electron.removeGameFromPinned(shop, objectId).then(() => { + showSuccessToast(t("game_removed_from_pinned")); + }); } else { if (!objectId) return; 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 4ef0b208..aa87f9b8 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -16,7 +16,13 @@ import "./profile-content.scss"; const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500; export function ProfileContent() { - const { userProfile, isMe, userStats } = useContext(userProfileContext); + const { + userProfile, + isMe, + userStats, + libraryGames, + pinnedGames, + } = useContext(userProfileContext); const [statsIndex, setStatsIndex] = useState(0); const [isAnimationRunning, setIsAnimationRunning] = useState(true); const statsAnimation = useRef(-1); @@ -68,13 +74,7 @@ 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; @@ -87,7 +87,7 @@ export function ProfileContent() { return ; } - const hasGames = userProfile?.libraryGames.length > 0; + const hasGames = libraryGames.length > 0; const hasPinnedGames = pinnedGames.length > 0; const shouldShowRightContent = hasGames || userProfile.friends.length > 0; @@ -108,7 +108,7 @@ export function ProfileContent() { {hasGames && ( <> {hasPinnedGames && ( -
+

{t("pinned")}

{pinnedGames.length} @@ -168,6 +168,8 @@ export function ProfileContent() { numberFormatter, t, statsIndex, + libraryGames, + pinnedGames, ]); 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 6b74d9b0..e64cd3bb 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 @@ -9,7 +9,12 @@ import { formatDownloadProgress, } from "@renderer/helpers"; import { userProfileContext } from "@renderer/context"; -import { ClockIcon, TrophyIcon, AlertFillIcon, HeartFillIcon } 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"; diff --git a/src/types/index.ts b/src/types/index.ts index 806ac40e..593c45be 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -73,8 +73,15 @@ export type UserGame = { hasManuallyUpdatedPlaytime: boolean; isFavorite: boolean; isPinned: boolean; + pinnedDate?: Date | null; } & ShopAssets; +export interface UserLibraryResponse { + totalCount: number; + library: UserGame[]; + pinnedGames: UserGame[]; +} + export interface GameRunning { id: string; title: string; diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 0f473935..da702b70 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -45,6 +45,7 @@ export interface Game { launchOptions?: string | null; favorite?: boolean; pinned?: boolean; + pinnedDate?: Date | null; automaticCloudSync?: boolean; hasManuallyUpdatedPlaytime?: boolean; }