feat:profile endpoint change and complete pinning functionality

This commit is contained in:
Moyasee
2025-09-24 14:04:55 +03:00
parent 33c15baf0e
commit 092af7e421
12 changed files with 96 additions and 23 deletions

View File

@@ -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";

View File

@@ -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);
registerEvent("addGameToPinned", addGameToPinned);

View File

@@ -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);
registerEvent("removeGameFromPinned", removeGameFromPinned);

View File

@@ -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<UserLibraryResponse | null> => {
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<UserLibraryResponse>(url).catch(() => null);
};
registerEvent("getUserLibrary", getUserLibrary);

View File

@@ -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) =>

View File

@@ -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<React.SetStateAction<string>>;
backgroundImage: string;
badges: Badge[];
libraryGames: UserGame[];
pinnedGames: UserGame[];
}
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
@@ -30,6 +32,8 @@ export const userProfileContext = createContext<UserProfileContext>({
setSelectedBackgroundImage: () => {},
backgroundImage: "",
badges: [],
libraryGames: [],
pinnedGames: [],
});
const { Provider } = userProfileContext;
@@ -49,6 +53,8 @@ export function UserProfileContextProvider({
const [userStats, setUserStats] = useState<UserStats | null>(null);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const [libraryGames, setLibraryGames] = useState<UserGame[]>([]);
const [pinnedGames, setPinnedGames] = useState<UserGame[]>([]);
const [badges, setBadges] = useState<Badge[]>([]);
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}

View File

@@ -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<void>;
addGameToPinned: (shop: GameShop, objectId: string) => Promise<void>;
removeGameFromPinned: (
shop: GameShop,
objectId: string
) => Promise<void>;
removeGameFromPinned: (shop: GameShop, objectId: string) => Promise<void>;
updateLaunchOptions: (
shop: GameShop,
objectId: string,
@@ -292,6 +291,7 @@ declare global {
/* User */
getUser: (userId: string) => Promise<UserProfile | null>;
getUserLibrary: (userId: string, take?: number, skip?: number) => Promise<UserLibraryResponse>;
blockUser: (userId: string) => Promise<void>;
unblockUser: (userId: string) => Promise<void>;
getUserFriends: (

View File

@@ -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;

View File

@@ -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 <LockedProfile />;
}
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 && (
<div style={{ marginBottom: '2rem' }}>
<div style={{ marginBottom: "2rem" }}>
<div className="profile-content__section-header">
<h2>{t("pinned")}</h2>
<span>{pinnedGames.length}</span>
@@ -168,6 +168,8 @@ export function ProfileContent() {
numberFormatter,
t,
statsIndex,
libraryGames,
pinnedGames,
]);
return (

View File

@@ -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";

View File

@@ -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;

View File

@@ -45,6 +45,7 @@ export interface Game {
launchOptions?: string | null;
favorite?: boolean;
pinned?: boolean;
pinnedDate?: Date | null;
automaticCloudSync?: boolean;
hasManuallyUpdatedPlaytime?: boolean;
}