mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 08:43:57 +00:00
feat:profile endpoint change and complete pinning functionality
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
27
src/main/events/user/get-user-library.ts
Normal file
27
src/main/events/user/get-user-library.ts
Normal 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);
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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}
|
||||
|
||||
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
@@ -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: (
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface Game {
|
||||
launchOptions?: string | null;
|
||||
favorite?: boolean;
|
||||
pinned?: boolean;
|
||||
pinnedDate?: Date | null;
|
||||
automaticCloudSync?: boolean;
|
||||
hasManuallyUpdatedPlaytime?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user