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 && (
)}
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 (
+ );
+ }, [
+ userProfile,
+ formatPlayTime,
+ numberFormatter,
+ t,
+ truncatedGamesList,
+ navigate,
+ ]);
+
+ return (
+
);
}
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 }}
>
-