From d9a7672113510a58b2bfed50f976952c2d050d76 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Fri, 13 Sep 2024 01:02:01 +0100 Subject: [PATCH] feat: profile redesign --- package.json | 2 +- src/main/events/catalogue/get-catalogue.ts | 3 +- src/main/events/profile/get-me.ts | 35 +++++++- src/main/events/user/get-user.ts | 1 - src/main/services/library-sync/create-game.ts | 5 +- .../confirmation-modal.css.ts | 2 +- .../src/components/game-card/game-card.tsx | 6 +- .../components/sidebar/sidebar-profile.tsx | 14 ++- .../src/components/sidebar/sidebar.tsx | 1 - .../game-details/game-details.context.tsx | 17 +++- .../game-details.context.types.ts | 9 +- .../src/features/user-details-slice.ts | 6 +- src/renderer/src/hooks/use-user-details.ts | 12 ++- .../pages/game-details/sidebar/sidebar.tsx | 12 ++- .../profile-content/locked-profile.css.ts | 24 +++++ .../profile-content/locked-profile.tsx | 18 ++++ .../profile-content/profile-content.tsx | 32 +++++-- .../profile/profile-hero/profile-hero.tsx | 90 ++++++++++++------- .../src/pages/profile/profile-skeleton.tsx | 11 ++- src/renderer/src/pages/profile/profile.css.ts | 48 +++++++++- .../user-edit-profile.tsx | 5 +- .../user-friend-modal-add-friend.tsx | 4 +- .../user-friend-modal-list.tsx | 2 +- src/types/index.ts | 6 -- 24 files changed, 268 insertions(+), 97 deletions(-) create mode 100644 src/renderer/src/pages/profile/profile-content/locked-profile.css.ts create mode 100644 src/renderer/src/pages/profile/profile-content/locked-profile.tsx diff --git a/package.json b/package.json index 1727e383..3a357df1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "2.0.3", + "version": "2.1.0", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", diff --git a/src/main/events/catalogue/get-catalogue.ts b/src/main/events/catalogue/get-catalogue.ts index 39fce7f5..cef9f887 100644 --- a/src/main/events/catalogue/get-catalogue.ts +++ b/src/main/events/catalogue/get-catalogue.ts @@ -15,7 +15,8 @@ const getCatalogue = async ( }); const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>( - `/games/${category}?${params.toString()}` + `/games/${category}?${params.toString()}`, + { needsAuth: false } ); return Promise.all( diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts index 1626125b..5154da8d 100644 --- a/src/main/events/profile/get-me.ts +++ b/src/main/events/profile/get-me.ts @@ -1,15 +1,33 @@ import { registerEvent } from "../register-event"; import * as Sentry from "@sentry/electron/main"; -import { HydraApi } from "@main/services"; +import { HydraApi, logger } from "@main/services"; import { UserProfile } from "@types"; import { userAuthRepository } from "@main/repository"; -import { UserNotLoggedInError } from "@shared"; +import { steamUrlBuilder, UserNotLoggedInError } from "@shared"; +import { steamGamesWorker } from "@main/workers"; + +const getSteamGame = async (objectId: string) => { + try { + const steamGame = await steamGamesWorker.run(Number(objectId), { + name: "getById", + }); + + return { + title: steamGame.name, + iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon), + }; + } catch (err) { + logger.error("Failed to get Steam game", err); + + return null; + } +}; const getMe = async ( _event: Electron.IpcMainInvokeEvent ): Promise => { return HydraApi.get(`/profile/me`) - .then((me) => { + .then(async (me) => { userAuthRepository.upsert( { id: 1, @@ -20,6 +38,17 @@ const getMe = async ( ["id"] ); + if (me.currentGame) { + const steamGame = await getSteamGame(me.currentGame.objectId); + + if (steamGame) { + me.currentGame = { + ...me.currentGame, + ...steamGame, + }; + } + } + Sentry.setUser({ id: me.id, username: me.username }); return me; diff --git a/src/main/events/user/get-user.ts b/src/main/events/user/get-user.ts index 8ee34b59..b7585298 100644 --- a/src/main/events/user/get-user.ts +++ b/src/main/events/user/get-user.ts @@ -81,7 +81,6 @@ const getUser = async ( totalFriends: friends.totalFriends, }; } catch (err) { - console.log("err", err); return null; } }; diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts index 48524e00..54c4e866 100644 --- a/src/main/services/library-sync/create-game.ts +++ b/src/main/services/library-sync/create-game.ts @@ -1,15 +1,14 @@ import { Game } from "@main/entity"; import { HydraApi } from "../hydra-api"; import { gameRepository } from "@main/repository"; +import { logger } from "../logger"; export const createGame = async (game: Game) => { - console.log({ objectId: game.objectID, shop: game.shop }); - HydraApi.post("/games/download", { objectId: game.objectID, shop: game.shop, }).catch((err) => { - console.log(err); + logger.error("Failed to create game download", err); }); HydraApi.post(`/profile/games`, { diff --git a/src/renderer/src/components/confirmation-modal/confirmation-modal.css.ts b/src/renderer/src/components/confirmation-modal/confirmation-modal.css.ts index 342a8350..a9aec403 100644 --- a/src/renderer/src/components/confirmation-modal/confirmation-modal.css.ts +++ b/src/renderer/src/components/confirmation-modal/confirmation-modal.css.ts @@ -1,4 +1,4 @@ -import { SPACING_UNIT, vars } from "../../theme.css"; +import { SPACING_UNIT } from "../../theme.css"; import { style } from "@vanilla-extract/css"; export const actions = style({ diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index aafa3a09..282c3d03 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -1,8 +1,4 @@ -import { - DownloadIcon, - FileDirectoryIcon, - PeopleIcon, -} from "@primer/octicons-react"; +import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; import type { CatalogueEntry, GameStats } from "@types"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index e7657c5e..984ec80d 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router-dom"; import { PeopleIcon, PersonIcon } from "@primer/octicons-react"; import * as styles from "./sidebar-profile.css"; -import { useAppSelector, useUserDetails } from "@renderer/hooks"; +import { useUserDetails } from "@renderer/hooks"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; @@ -17,8 +17,6 @@ export function SidebarProfile() { return friendRequests.filter((request) => request.type === "RECEIVED"); }, [friendRequests]); - const { gameRunning } = useAppSelector((state) => state.gameRunning); - const handleProfileClick = () => { if (userDetails === null) { window.electron.openAuthWindow(); @@ -76,19 +74,19 @@ export function SidebarProfile() { {userDetails ? userDetails.displayName : t("sign_in")}

- {userDetails && gameRunning && ( + {userDetails && userDetails.currentGame && (
- {gameRunning.title} + {userDetails.currentGame.title}
)} - {userDetails && gameRunning?.iconUrl && ( + {userDetails && userDetails.currentGame && ( {gameRunning.title} )} diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index fc2a4d14..aa89ff02 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -15,7 +15,6 @@ import { buildGameDetailsPath } from "@renderer/helpers"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import { SidebarProfile } from "./sidebar-profile"; import { sortBy } from "lodash-es"; -import { ChevronDownIcon } from "@primer/octicons-react"; const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_INITIAL_WIDTH = 250; diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index fd3e4600..84631eb9 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -5,7 +5,13 @@ import { setHeaderTitle } from "@renderer/features"; import { getSteamLanguage } from "@renderer/helpers"; import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks"; -import type { Game, GameRepack, GameShop, ShopDetails } from "@types"; +import type { + Game, + GameRepack, + GameShop, + GameStats, + ShopDetails, +} from "@types"; import { useTranslation } from "react-i18next"; import { GameDetailsContext } from "./game-details.context.types"; @@ -22,6 +28,7 @@ export const gameDetailsContext = createContext({ gameColor: "", showRepacksModal: false, showGameOptionsModal: false, + stats: null, setGameColor: () => {}, selectGameExecutable: async () => null, updateGame: async () => {}, @@ -45,6 +52,8 @@ export function GameDetailsContextProvider({ const [repacks, setRepacks] = useState([]); const [game, setGame] = useState(null); + const [stats, setStats] = useState(null); + const [isLoading, setIsLoading] = useState(false); const [gameColor, setGameColor] = useState(""); const [isGameRunning, setisGameRunning] = useState(false); @@ -85,13 +94,16 @@ export function GameDetailsContextProvider({ getSteamLanguage(i18n.language) ), window.electron.searchGameRepacks(gameTitle), + window.electron.getGameStats(objectID!, shop as GameShop), ]) - .then(([appDetailsResult, repacksResult]) => { + .then(([appDetailsResult, repacksResult, statsResult]) => { if (appDetailsResult.status === "fulfilled") setGameDetails(appDetailsResult.value); if (repacksResult.status === "fulfilled") setRepacks(repacksResult.value); + + if (statsResult.status === "fulfilled") setStats(statsResult.value); }) .finally(() => { setIsLoading(false); @@ -167,6 +179,7 @@ export function GameDetailsContextProvider({ gameColor, showGameOptionsModal, showRepacksModal, + stats, setGameColor, selectGameExecutable, updateGame, diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index 36e55a79..79f140c9 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -1,4 +1,10 @@ -import type { Game, GameRepack, GameShop, ShopDetails } from "@types"; +import type { + Game, + GameRepack, + GameShop, + GameStats, + ShopDetails, +} from "@types"; export interface GameDetailsContext { game: Game | null; @@ -12,6 +18,7 @@ export interface GameDetailsContext { gameColor: string; showRepacksModal: boolean; showGameOptionsModal: boolean; + stats: GameStats | null; setGameColor: React.Dispatch>; selectGameExecutable: () => Promise; updateGame: () => Promise; diff --git a/src/renderer/src/features/user-details-slice.ts b/src/renderer/src/features/user-details-slice.ts index d559de09..00542020 100644 --- a/src/renderer/src/features/user-details-slice.ts +++ b/src/renderer/src/features/user-details-slice.ts @@ -1,9 +1,9 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; -import type { FriendRequest, UserDetails } from "@types"; +import type { FriendRequest, UserProfile } from "@types"; export interface UserDetailsState { - userDetails: UserDetails | null; + userDetails: UserProfile | null; profileBackground: null | string; friendRequests: FriendRequest[]; isFriendsModalVisible: boolean; @@ -24,7 +24,7 @@ export const userDetailsSlice = createSlice({ name: "user-details", initialState, reducers: { - setUserDetails: (state, action: PayloadAction) => { + setUserDetails: (state, action: PayloadAction) => { state.userDetails = action.payload; }, setProfileBackground: (state, action: PayloadAction) => { diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index e1574271..6b5b09bc 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -7,14 +7,12 @@ import { setFriendsModalVisible, setFriendsModalHidden, } from "@renderer/features"; -// import { profileBackgroundFromProfileImage } from "@renderer/helpers"; import type { FriendRequestAction, UpdateProfileRequest, - UserDetails, + UserProfile, } from "@types"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; -import { logger } from "@renderer/logger"; export function useUserDetails() { const dispatch = useAppDispatch(); @@ -42,17 +40,18 @@ export function useUserDetails() { }, [clearUserDetails]); const updateUserDetails = useCallback( - async (userDetails: UserDetails) => { + async (userDetails: UserProfile) => { dispatch(setUserDetails(userDetails)); if (userDetails.profileImageUrl) { + // TODO: Decide if we want to use this // const profileBackground = await profileBackgroundFromProfileImage( // userDetails.profileImageUrl // ).catch((err) => { // logger.error("profileBackgroundFromProfileImage", err); // return `#151515B3`; // }); - dispatch(setProfileBackground(profileBackground)); + // dispatch(setProfileBackground(profileBackground)); window.localStorage.setItem( "userDetails", @@ -68,7 +67,7 @@ export function useUserDetails() { ); } }, - [dispatch] + [dispatch, profileBackground] ); const fetchUserDetails = useCallback(async () => { @@ -83,7 +82,6 @@ export function useUserDetails() { const patchUser = useCallback( async (values: UpdateProfileRequest) => { - console.log("values", values); const response = await window.electron.updateProfile(values); return updateUserDetails(response); }, diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 6c963ee9..837d1fca 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -16,7 +16,8 @@ export function Sidebar() { const [activeRequirement, setActiveRequirement] = useState("minimum"); - const { gameTitle, shopDetails, objectID } = useContext(gameDetailsContext); + const { gameTitle, shopDetails, objectID, stats } = + useContext(gameDetailsContext); const { t } = useTranslation("game_details"); @@ -42,6 +43,15 @@ export function Sidebar() { isLoading={howLongToBeat.isLoading} /> +
+

{t("stats")}

+
+ +
+

downloadCount {stats?.downloadCount}

+

playerCount {stats?.playerCount}

+
+

{t("requirements")}

diff --git a/src/renderer/src/pages/profile/profile-content/locked-profile.css.ts b/src/renderer/src/pages/profile/profile-content/locked-profile.css.ts new file mode 100644 index 00000000..bf5494b6 --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/locked-profile.css.ts @@ -0,0 +1,24 @@ +import { SPACING_UNIT } from "../../../theme.css"; + +import { style } from "@vanilla-extract/css"; + +export const container = style({ + display: "flex", + width: "100%", + height: "100%", + justifyContent: "center", + alignItems: "center", + flexDirection: "column", + gap: `${SPACING_UNIT}px`, +}); + +export const lockIcon = style({ + width: "60px", + height: "60px", + borderRadius: "50%", + backgroundColor: "rgba(255, 255, 255, 0.06)", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: `${SPACING_UNIT * 2}px`, +}); diff --git a/src/renderer/src/pages/profile/profile-content/locked-profile.tsx b/src/renderer/src/pages/profile/profile-content/locked-profile.tsx new file mode 100644 index 00000000..8edf4435 --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/locked-profile.tsx @@ -0,0 +1,18 @@ +import { LockIcon } from "@primer/octicons-react"; +import { useTranslation } from "react-i18next"; + +import * as styles from "./locked-profile.css"; + +export function LockedProfile() { + const { t } = useTranslation("user_profile"); + + return ( +
+
+ +
+

{t("locked_profile")}

+

{t("locked_profile_description")}

+
+ ); +} 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 88910582..99ba778b 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -4,7 +4,7 @@ import { ProfileHero } from "../profile-hero/profile-hero"; import { useAppDispatch } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; import { steamUrlBuilder } from "@shared"; -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT } from "@renderer/theme.css"; import * as styles from "./profile-content.css"; import { ClockIcon } from "@primer/octicons-react"; @@ -14,6 +14,7 @@ import { UserGame } from "@types"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { buildGameDetailsPath } from "@renderer/helpers"; import { useNavigate } from "react-router-dom"; +import { LockedProfile } from "./locked-profile"; export function ProfileContent() { const { userProfile } = useContext(userProfileContext); @@ -64,12 +65,14 @@ export function ProfileContent() { objectID: game.objectId, }); - if (!userProfile) return null; + const content = useMemo(() => { + if (!userProfile) return null; - return ( -
- + if (userProfile?.profileVisibility === "FRIENDS") { + return ; + } + return (
{game.title} {friend.displayName}
+ ); + }, [ + userProfile, + formatPlayTime, + numberFormatter, + t, + truncatedGamesList, + navigate, + ]); + + return ( +
+ + + {content}
); } diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index 379a2bcb..272d140d 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -1,7 +1,7 @@ import { SPACING_UNIT } from "@renderer/theme.css"; import * as styles from "./profile-hero.css"; -import { useContext, useMemo, useState } from "react"; +import { useCallback, useContext, useMemo, useState } from "react"; import { userProfileContext } from "@renderer/context"; import { CheckCircleFillIcon, @@ -48,39 +48,53 @@ export function ProfileHero() { const navigate = useNavigate(); - const handleSignOut = async () => { + const handleSignOut = useCallback(async () => { await signOut(); showSuccessToast(t("successfully_signed_out")); navigate("/"); - }; + }, [navigate, signOut, showSuccessToast, t]); - const handleFriendAction = (userId: string, action: FriendAction) => { - try { - if (action === "UNDO_FRIENDSHIP") { - undoFriendship(userId).then(getUserProfile); - return; + const handleFriendAction = useCallback( + (userId: string, action: FriendAction) => { + try { + if (action === "UNDO_FRIENDSHIP") { + undoFriendship(userId).then(getUserProfile); + return; + } + + if (action === "BLOCK") { + blockUser(userId).then(() => { + showSuccessToast(t("user_blocked_successfully")); + navigate(-1); + }); + + return; + } + + if (action === "SEND") { + sendFriendRequest(userProfile.id).then(getUserProfile); + return; + } + + updateFriendRequestState(userId, action).then(getUserProfile); + } catch (err) { + showErrorToast(t("try_again")); } - - if (action === "BLOCK") { - blockUser(userId).then(() => { - showSuccessToast(t("user_blocked_successfully")); - navigate(-1); - }); - - return; - } - - if (action === "SEND") { - sendFriendRequest(userProfile.id).then(getUserProfile); - return; - } - - updateFriendRequestState(userId, action).then(getUserProfile); - } catch (err) { - showErrorToast(t("try_again")); - } - }; + }, + [ + undoFriendship, + blockUser, + sendFriendRequest, + updateFriendRequestState, + t, + showErrorToast, + getUserProfile, + navigate, + showSuccessToast, + userProfile.id, + ] + ); const profileActions = useMemo(() => { if (isMe) { @@ -139,7 +153,7 @@ export function ProfileHero() { handleFriendAction(userProfile.relation!.BId, "CANCEL") } > - {t("cancel_request")} + {t("cancel_request")} ); } @@ -152,7 +166,7 @@ export function ProfileHero() { handleFriendAction(userProfile.relation!.AId, "ACCEPTED") } > - {t("accept_request")} + {t("accept_request")} ); - }, []); + }, [handleFriendAction, handleSignOut, isMe, t, userProfile]); + + const handleAvatarClick = useCallback(() => { + if (isMe) { + setShowEditProfileModal(true); + } + }, [isMe]); return ( <> @@ -188,7 +208,11 @@ export function ProfileHero() { style={{ background: heroBackground }} >
- diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx index b6e6aaea..91126923 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx @@ -42,13 +42,13 @@ export const UserFriendModalAddFriend = ({ const handleClickRequest = (userId: string) => { closeModal(); - navigate(`/user/${userId}`); + navigate(`/profile/${userId}`); }; const handleClickSeeProfile = () => { closeModal(); if (friendCode.length === 8) { - navigate(`/user/${friendCode}`); + navigate(`/profile/${friendCode}`); } }; diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx index 8ef96baf..36ff7e14 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx @@ -80,7 +80,7 @@ export const UserFriendModalList = ({ const handleClickFriend = (userId: string) => { closeModal(); - navigate(`/user/${userId}`); + navigate(`/profile/${userId}`); }; const handleUndoFriendship = (userId: string) => { diff --git a/src/types/index.ts b/src/types/index.ts index 34741751..a2ca7409 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -270,12 +270,6 @@ export interface RealDebridUser { expiration: string; } -export interface UserDetails { - id: string; - displayName: string; - profileImageUrl: string | null; -} - export interface UserFriend { id: string; displayName: string;