mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
feat: initial profile refactor
This commit is contained in:
parent
6273ca1376
commit
ada7b452a0
9712
pnpm-lock.yaml
generated
Normal file
9712
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,8 @@
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import type { CatalogueEntry, GameShop } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { RepacksManager, requestSteam250 } from "@main/services";
|
||||
import { formatName } from "@shared";
|
||||
import { formatName, steamUrlBuilder } from "@shared";
|
||||
|
||||
const resultSize = 12;
|
||||
|
||||
@ -24,7 +23,7 @@ const getCatalogue = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
objectID,
|
||||
title,
|
||||
shop: "steam" as GameShop,
|
||||
cover: getSteamAppAsset("library", objectID),
|
||||
cover: steamUrlBuilder.library(objectID),
|
||||
};
|
||||
|
||||
results.push({ ...catalogueEntry, repacks });
|
||||
|
@ -3,9 +3,9 @@ import flexSearch from "flexsearch";
|
||||
|
||||
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
||||
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { RepacksManager } from "@main/services";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
export interface SearchGamesArgs {
|
||||
query?: string;
|
||||
@ -19,7 +19,7 @@ export const convertSteamGameToCatalogueEntry = (
|
||||
objectID: String(game.id),
|
||||
title: game.name,
|
||||
shop: "steam" as GameShop,
|
||||
cover: getSteamAppAsset("library", String(game.id)),
|
||||
cover: steamUrlBuilder.library(String(game.id)),
|
||||
repacks: [],
|
||||
});
|
||||
|
||||
|
@ -3,10 +3,11 @@ import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { GameShop } from "@types";
|
||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||
import { getFileBase64 } from "@main/helpers";
|
||||
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -32,7 +33,7 @@ const addGameToLibrary = async (
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import axios from "axios";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileTypeFromFile } from "file-type";
|
||||
import { UpdateProfileProps, UserProfile } from "@types";
|
||||
import type { UpdateProfileRequest, UserProfile } from "@types";
|
||||
|
||||
const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
|
||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||
return HydraApi.patch("/profile", updateProfile);
|
||||
};
|
||||
|
||||
const updateProfile = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
updateProfile: UpdateProfileProps
|
||||
updateProfile: UpdateProfileRequest
|
||||
): Promise<UserProfile> => {
|
||||
if (!updateProfile.profileImageUrl) {
|
||||
return patchUserProfile(updateProfile);
|
||||
@ -40,7 +40,11 @@ const updateProfile = async (
|
||||
});
|
||||
return profileImageUrl as string;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
.catch((err) => {
|
||||
logger.error("Error uploading profile image", err);
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||
};
|
||||
|
@ -7,12 +7,13 @@ import {
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||
import { getFileBase64 } from "@main/helpers";
|
||||
import { DownloadManager } from "@main/services";
|
||||
|
||||
import { Not } from "typeorm";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -65,7 +66,7 @@ const startGameDownload = async (
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { GameRunning, UserGame, UserProfile } from "@types";
|
||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import type { UserProfile } from "@types";
|
||||
import { getUserFriends } from "./get-user-friends";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const getUser = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -12,65 +11,47 @@ const getUser = async (
|
||||
): Promise<UserProfile | null> => {
|
||||
try {
|
||||
const [profile, friends] = await Promise.all([
|
||||
HydraApi.get(`/users/${userId}`),
|
||||
HydraApi.get<UserProfile | null>(`/users/${userId}`),
|
||||
getUserFriends(userId, 12, 0).catch(() => {
|
||||
return { totalFriends: 0, friends: [] };
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!profile) return null;
|
||||
|
||||
const recentGames = await Promise.all(
|
||||
profile.recentGames.map(async (game) => {
|
||||
return getSteamUserGame(game);
|
||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
return {
|
||||
...game,
|
||||
title: steamGame.name,
|
||||
iconUrl: steamUrlBuilder.icon(game.objectId, steamGame.clientIcon),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const libraryGames = await Promise.all(
|
||||
profile.libraryGames.map(async (game) => {
|
||||
return getSteamUserGame(game);
|
||||
})
|
||||
);
|
||||
// const libraryGames = await Promise.all(
|
||||
// profile.libraryGames.map(async (game) => {
|
||||
// return getSteamUserGame(game);
|
||||
// })
|
||||
// );
|
||||
|
||||
const currentGame = await getGameRunning(profile.currentGame);
|
||||
// const currentGame = await getGameRunning(profile.currentGame);
|
||||
|
||||
return {
|
||||
...profile,
|
||||
libraryGames,
|
||||
// libraryGames,
|
||||
recentGames,
|
||||
friends: friends.friends,
|
||||
totalFriends: friends.totalFriends,
|
||||
currentGame,
|
||||
// currentGame,
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getGameRunning = async (currentGame): Promise<GameRunning | null> => {
|
||||
if (!currentGame) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gameRunning = await getSteamUserGame(currentGame);
|
||||
|
||||
return {
|
||||
...gameRunning,
|
||||
sessionDurationInMillis: currentGame.sessionDurationInSeconds * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
const getSteamUserGame = async (game): Promise<UserGame> => {
|
||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...game,
|
||||
...convertSteamGameToCatalogueEntry(steamGame),
|
||||
iconUrl,
|
||||
};
|
||||
};
|
||||
|
||||
registerEvent("getUser", getUser);
|
||||
|
@ -2,23 +2,6 @@ import axios from "axios";
|
||||
import { JSDOM } from "jsdom";
|
||||
import UserAgent from "user-agents";
|
||||
|
||||
export const getSteamAppAsset = (
|
||||
category: "library" | "hero" | "logo" | "icon",
|
||||
objectID: string,
|
||||
clientIcon?: string
|
||||
) => {
|
||||
if (category === "library")
|
||||
return `https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`;
|
||||
|
||||
if (category === "hero")
|
||||
return `https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/library_hero.jpg`;
|
||||
|
||||
if (category === "logo")
|
||||
return `https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`;
|
||||
|
||||
return `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${objectID}/${clientIcon}.ico`;
|
||||
};
|
||||
|
||||
export const getFileBuffer = async (url: string) =>
|
||||
fetch(url, { method: "GET" }).then((response) =>
|
||||
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
||||
@ -34,15 +17,6 @@ export const getFileBase64 = async (url: string) =>
|
||||
})
|
||||
);
|
||||
|
||||
export const steamUrlBuilder = {
|
||||
library: (objectID: string) =>
|
||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
|
||||
libraryHero: (objectID: string) =>
|
||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/library_hero.jpg`,
|
||||
logo: (objectID: string) =>
|
||||
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
|
||||
};
|
||||
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
|
@ -77,54 +77,54 @@ export class HydraApi {
|
||||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||
});
|
||||
|
||||
this.instance.interceptors.request.use(
|
||||
(request) => {
|
||||
logger.log(" ---- REQUEST -----");
|
||||
logger.log(request.method, request.url, request.params, request.data);
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
logger.error("request error", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// this.instance.interceptors.request.use(
|
||||
// (request) => {
|
||||
// logger.log(" ---- REQUEST -----");
|
||||
// logger.log(request.method, request.url, request.params, request.data);
|
||||
// return request;
|
||||
// },
|
||||
// (error) => {
|
||||
// logger.error("request error", error);
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
logger.log(" ---- RESPONSE -----");
|
||||
logger.log(
|
||||
response.status,
|
||||
response.config.method,
|
||||
response.config.url,
|
||||
response.data
|
||||
);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
logger.error(" ---- RESPONSE ERROR -----");
|
||||
// this.instance.interceptors.response.use(
|
||||
// (response) => {
|
||||
// logger.log(" ---- RESPONSE -----");
|
||||
// logger.log(
|
||||
// response.status,
|
||||
// response.config.method,
|
||||
// response.config.url,
|
||||
// response.data
|
||||
// );
|
||||
// return response;
|
||||
// },
|
||||
// (error) => {
|
||||
// logger.error(" ---- RESPONSE ERROR -----");
|
||||
|
||||
const { config } = error;
|
||||
// const { config } = error;
|
||||
|
||||
logger.error(
|
||||
config.method,
|
||||
config.baseURL,
|
||||
config.url,
|
||||
config.headers,
|
||||
config.data
|
||||
);
|
||||
// logger.error(
|
||||
// config.method,
|
||||
// config.baseURL,
|
||||
// config.url,
|
||||
// config.headers,
|
||||
// config.data
|
||||
// );
|
||||
|
||||
if (error.response) {
|
||||
logger.error("Response", error.response.status, error.response.data);
|
||||
} else if (error.request) {
|
||||
logger.error("Request", error.request);
|
||||
} else {
|
||||
logger.error("Error", error.message);
|
||||
}
|
||||
// if (error.response) {
|
||||
// logger.error("Response", error.response.status, error.response.data);
|
||||
// } else if (error.request) {
|
||||
// logger.error("Request", error.request);
|
||||
// } else {
|
||||
// logger.error("Error", error.message);
|
||||
// }
|
||||
|
||||
logger.error(" ----- END RESPONSE ERROR -------");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// logger.error(" ----- END RESPONSE ERROR -------");
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
|
||||
const userAuth = await userAuthRepository.findOne({
|
||||
where: { id: 1 },
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
export const mergeWithRemoteGames = async () => {
|
||||
return HydraApi.get("/profile/games")
|
||||
@ -44,7 +44,7 @@ export const mergeWithRemoteGames = async () => {
|
||||
|
||||
if (steamGame) {
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
gameRepository.insert({
|
||||
|
@ -10,7 +10,7 @@ import type {
|
||||
StartGameDownloadPayload,
|
||||
GameRunning,
|
||||
FriendRequestAction,
|
||||
UpdateProfileProps,
|
||||
UpdateProfileRequest,
|
||||
} from "@types";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
@ -138,7 +138,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
getMe: () => ipcRenderer.invoke("getMe"),
|
||||
undoFriendship: (userId: string) =>
|
||||
ipcRenderer.invoke("undoFriendship", userId),
|
||||
updateProfile: (updateProfile: UpdateProfileProps) =>
|
||||
updateProfile: (updateProfile: UpdateProfileRequest) =>
|
||||
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||
|
@ -39,7 +39,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (location.pathname.startsWith("/game")) return headerTitle;
|
||||
if (location.pathname.startsWith("/user")) return headerTitle;
|
||||
if (location.pathname.startsWith("/profile")) return headerTitle;
|
||||
if (location.pathname.startsWith("/search")) return t("search_results");
|
||||
|
||||
return t(pathTitle[location.pathname]);
|
||||
|
@ -2,12 +2,9 @@ import { useNavigate } from "react-router-dom";
|
||||
import * as styles from "./hero.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ShopDetails } from "@types";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
getSteamLanguage,
|
||||
steamUrlBuilder,
|
||||
} from "@renderer/helpers";
|
||||
import { buildGameDetailsPath, getSteamLanguage } from "@renderer/helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const FEATURED_GAME_TITLE = "ELDEN RING";
|
||||
const FEATURED_GAME_ID = "1245620";
|
||||
|
@ -1,21 +1,13 @@
|
||||
import { createVar, style } from "@vanilla-extract/css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const profileContainerBackground = createVar();
|
||||
|
||||
export const profileContainer = style({
|
||||
background: profileContainerBackground,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const profileButton = style({
|
||||
@ -25,13 +17,17 @@ export const profileButton = style({
|
||||
color: vars.color.muted,
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const profileButtonContent = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
height: "40px",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
@ -77,14 +73,31 @@ export const profileButtonTitle = style({
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
export const friendRequestButton = style({
|
||||
color: vars.color.success,
|
||||
export const friendsButton = style({
|
||||
color: vars.color.muted,
|
||||
cursor: "pointer",
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
width: "40px",
|
||||
minWidth: "40px",
|
||||
minHeight: "40px",
|
||||
height: "40px",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
transition: "all ease 0.3s",
|
||||
":hover": {
|
||||
color: vars.color.muted,
|
||||
backgroundColor: "#DADBE1",
|
||||
},
|
||||
});
|
||||
|
||||
export const friendsButtonLabel = style({
|
||||
backgroundColor: vars.color.success,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
top: "-5px",
|
||||
right: "-5px",
|
||||
});
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PersonAddIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
import { assignInlineVars } from "@vanilla-extract/dynamic";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { profileContainerBackground } from "./sidebar-profile.css";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { FriendRequest } from "@types";
|
||||
|
||||
@ -14,8 +12,7 @@ export function SidebarProfile() {
|
||||
|
||||
const { t } = useTranslation("sidebar");
|
||||
|
||||
const { userDetails, profileBackground, friendRequests, showFriendsModal } =
|
||||
useUserDetails();
|
||||
const { userDetails, friendRequests, showFriendsModal } = useUserDetails();
|
||||
|
||||
const [receivedRequests, setReceivedRequests] = useState<FriendRequest[]>([]);
|
||||
|
||||
@ -33,24 +30,11 @@ export function SidebarProfile() {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/user/${userDetails!.id}`);
|
||||
navigate(`/profile/${userDetails!.id}`);
|
||||
};
|
||||
|
||||
const profileButtonBackground = useMemo(() => {
|
||||
if (profileBackground) return profileBackground;
|
||||
return undefined;
|
||||
}, [profileBackground]);
|
||||
|
||||
const showPendingRequests =
|
||||
userDetails && receivedRequests.length > 0 && !gameRunning;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.profileContainer}
|
||||
style={assignInlineVars({
|
||||
[profileContainerBackground]: profileButtonBackground,
|
||||
})}
|
||||
>
|
||||
<div className={styles.profileContainer}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileButton}
|
||||
@ -91,18 +75,18 @@ export function SidebarProfile() {
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
{showPendingRequests && (
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.friendRequestButton}
|
||||
className={styles.friendsButton}
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||
}
|
||||
>
|
||||
<PersonAddIcon size={24} />
|
||||
{receivedRequests.length}
|
||||
<small className={styles.friendsButtonLabel}>10</small>
|
||||
|
||||
<PeopleIcon size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -24,22 +24,13 @@ export const sidebar = recipe({
|
||||
},
|
||||
});
|
||||
|
||||
export const content = recipe({
|
||||
base: {
|
||||
export const content = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
},
|
||||
variants: {
|
||||
macos: {
|
||||
true: {
|
||||
paddingTop: `${SPACING_UNIT * 6}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const handle = style({
|
||||
|
@ -15,6 +15,7 @@ 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;
|
||||
@ -157,17 +158,12 @@ export function Sidebar() {
|
||||
width: sidebarWidth,
|
||||
minWidth: sidebarWidth,
|
||||
maxWidth: sidebarWidth,
|
||||
paddingTop: 8 * 6,
|
||||
}}
|
||||
>
|
||||
<SidebarProfile />
|
||||
|
||||
<div
|
||||
className={styles.content({
|
||||
macos: window.electron.platform === "darwin",
|
||||
})}
|
||||
>
|
||||
{window.electron.platform === "darwin" && <h2>Hydra</h2>}
|
||||
|
||||
<div className={styles.content}>
|
||||
<section className={styles.section}>
|
||||
<ul className={styles.menu}>
|
||||
{routes.map(({ nameKey, path, render }) => (
|
||||
@ -184,6 +180,8 @@ export function Sidebar() {
|
||||
>
|
||||
{render(isDownloading)}
|
||||
<span>{t(nameKey)}</span>
|
||||
|
||||
<ChevronDownIcon />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from "./game-details/game-details.context";
|
||||
export * from "./settings/settings.context";
|
||||
export * from "./user-profile/user-profile.context";
|
||||
|
@ -0,0 +1,90 @@
|
||||
import { darkenColor } from "@renderer/helpers";
|
||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||
import type { UserProfile } from "@types";
|
||||
import { average } from "color.js";
|
||||
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export interface UserProfileContext {
|
||||
userProfile: UserProfile | null;
|
||||
heroBackground: string;
|
||||
/* Indicates if the current user is viewing their own profile */
|
||||
isMe: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
||||
|
||||
export const userProfileContext = createContext<UserProfileContext>({
|
||||
userProfile: null,
|
||||
heroBackground: DEFAULT_USER_PROFILE_BACKGROUND,
|
||||
isMe: false,
|
||||
});
|
||||
|
||||
const { Provider } = userProfileContext;
|
||||
export const { Consumer: UserProfileContextConsumer } = userProfileContext;
|
||||
|
||||
export interface UserProfileContextProviderProps {
|
||||
children: React.ReactNode;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export function UserProfileContextProvider({
|
||||
children,
|
||||
userId,
|
||||
}: UserProfileContextProviderProps) {
|
||||
const { userDetails } = useAppSelector((state) => state.userDetails);
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
const [heroBackground, setHeroBackground] = useState(
|
||||
DEFAULT_USER_PROFILE_BACKGROUND
|
||||
);
|
||||
|
||||
const getHeroBackgroundFromImageUrl = async (imageUrl: string) => {
|
||||
const output = await average(imageUrl, {
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
});
|
||||
|
||||
return `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
||||
};
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const { showErrorToast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getUserProfile = useCallback(async () => {
|
||||
return window.electron.getUser(userId).then((userProfile) => {
|
||||
if (userProfile) {
|
||||
setUserProfile(userProfile);
|
||||
|
||||
if (userProfile.profileImageUrl) {
|
||||
getHeroBackgroundFromImageUrl(userProfile.profileImageUrl).then(
|
||||
(color) => setHeroBackground(color)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showErrorToast(t("user_not_found"));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}, [navigate, showErrorToast, userId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
getUserProfile();
|
||||
}, [getUserProfile]);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
userProfile,
|
||||
heroBackground,
|
||||
isMe: userDetails?.id === userProfile?.id,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
}
|
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@ -18,6 +18,7 @@ import type {
|
||||
FriendRequestAction,
|
||||
UserFriends,
|
||||
UserBlocks,
|
||||
UpdateProfileRequest,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
@ -141,7 +142,9 @@ declare global {
|
||||
/* Profile */
|
||||
getMe: () => Promise<UserProfile | null>;
|
||||
undoFriendship: (userId: string) => Promise<void>;
|
||||
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||
updateProfile: (
|
||||
updateProfile: UpdateProfileRequest
|
||||
) => Promise<UserProfile>;
|
||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||
updateFriendRequest: (
|
||||
userId: string,
|
||||
|
@ -1,16 +1,6 @@
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
import Color from "color";
|
||||
import { average } from "color.js";
|
||||
|
||||
export const steamUrlBuilder = {
|
||||
library: (objectID: string) =>
|
||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
|
||||
libraryHero: (objectID: string) =>
|
||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/library_hero.jpg`,
|
||||
logo: (objectID: string) =>
|
||||
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
|
||||
};
|
||||
|
||||
export const formatDownloadProgress = (progress?: number) => {
|
||||
if (!progress) return "0%";
|
||||
@ -46,14 +36,3 @@ export const buildGameDetailsPath = (
|
||||
|
||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||
new Color(color).darken(amount).alpha(alpha).toString();
|
||||
|
||||
export const profileBackgroundFromProfileImage = async (
|
||||
profileImageUrl: string
|
||||
) => {
|
||||
const output = await average(profileImageUrl, {
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
});
|
||||
|
||||
return `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
||||
};
|
||||
|
0
src/renderer/src/hooks/use-friendship.ts
Normal file
0
src/renderer/src/hooks/use-friendship.ts
Normal file
@ -7,8 +7,12 @@ import {
|
||||
setFriendsModalVisible,
|
||||
setFriendsModalHidden,
|
||||
} from "@renderer/features";
|
||||
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
||||
import { FriendRequestAction, UpdateProfileProps, UserDetails } from "@types";
|
||||
// import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
||||
import type {
|
||||
FriendRequestAction,
|
||||
UpdateProfileRequest,
|
||||
UserDetails,
|
||||
} from "@types";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { logger } from "@renderer/logger";
|
||||
|
||||
@ -42,12 +46,12 @@ export function useUserDetails() {
|
||||
dispatch(setUserDetails(userDetails));
|
||||
|
||||
if (userDetails.profileImageUrl) {
|
||||
const profileBackground = await profileBackgroundFromProfileImage(
|
||||
userDetails.profileImageUrl
|
||||
).catch((err) => {
|
||||
logger.error("profileBackgroundFromProfileImage", err);
|
||||
return `#151515B3`;
|
||||
});
|
||||
// const profileBackground = await profileBackgroundFromProfileImage(
|
||||
// userDetails.profileImageUrl
|
||||
// ).catch((err) => {
|
||||
// logger.error("profileBackgroundFromProfileImage", err);
|
||||
// return `#151515B3`;
|
||||
// });
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
|
||||
window.localStorage.setItem(
|
||||
@ -78,8 +82,9 @@ export function useUserDetails() {
|
||||
}, [clearUserDetails]);
|
||||
|
||||
const patchUser = useCallback(
|
||||
async (props: UpdateProfileProps) => {
|
||||
const response = await window.electron.updateProfile(props);
|
||||
async (values: UpdateProfileRequest) => {
|
||||
console.log("values", values);
|
||||
const response = await window.electron.updateProfile(values);
|
||||
return updateUserDetails(response);
|
||||
},
|
||||
[updateUserDetails]
|
||||
|
@ -22,12 +22,12 @@ import {
|
||||
SearchResults,
|
||||
Settings,
|
||||
Catalogue,
|
||||
Profile,
|
||||
} from "@renderer/pages";
|
||||
|
||||
import { store } from "./store";
|
||||
|
||||
import * as resources from "@locales";
|
||||
import { User } from "./pages/user/user";
|
||||
|
||||
Sentry.init({});
|
||||
|
||||
@ -57,7 +57,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/user/:userId" Component={User} />
|
||||
<Route path="/profile/:userId" Component={Profile} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
@ -6,10 +6,9 @@ import { Badge, Button } from "@renderer/components";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
formatDownloadProgress,
|
||||
steamUrlBuilder,
|
||||
} from "@renderer/helpers";
|
||||
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||
|
||||
|
@ -2,8 +2,6 @@ import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { average } from "color.js";
|
||||
import Color from "color";
|
||||
|
||||
import { steamUrlBuilder } from "@renderer/helpers";
|
||||
|
||||
import { HeroPanel } from "./hero";
|
||||
import { DescriptionHeader } from "./description-header/description-header";
|
||||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||
@ -12,6 +10,7 @@ import { Sidebar } from "./sidebar/sidebar";
|
||||
import * as styles from "./game-details.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const HERO_ANIMATION_THRESHOLD = 25;
|
||||
|
||||
|
@ -4,3 +4,4 @@ export * from "./downloads/downloads";
|
||||
export * from "./home/search-results";
|
||||
export * from "./settings/settings";
|
||||
export * from "./catalogue/catalogue";
|
||||
export * from "./profile/profile";
|
||||
|
73
src/renderer/src/pages/profile/profile-content.css.ts
Normal file
73
src/renderer/src/pages/profile/profile-content.css.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { vars, SPACING_UNIT } from "../../theme.css";
|
||||
import { globalStyle, style } from "@vanilla-extract/css";
|
||||
|
||||
export const gameCover = style({
|
||||
transition: "all ease 0.2s",
|
||||
boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)",
|
||||
":before": {
|
||||
content: "",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "172%",
|
||||
position: "absolute",
|
||||
background:
|
||||
"linear-gradient(35deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 51.5%, rgba(255, 255, 255, 0.15) 54%, rgba(255, 255, 255, 0.15) 100%);",
|
||||
transition: "all ease 0.3s",
|
||||
transform: "translateY(-36%)",
|
||||
opacity: "0.5",
|
||||
},
|
||||
});
|
||||
|
||||
export const game = style({
|
||||
transition: "all ease 0.2s",
|
||||
":hover": {
|
||||
transform: "scale(1.05)",
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${gameCover}:hover::before`, {
|
||||
opacity: "1",
|
||||
transform: "translateY(-20%)",
|
||||
});
|
||||
|
||||
export const box = style({
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const sectionHeader = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const list = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const friend = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const friendAvatar = style({
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const friendName = style({
|
||||
color: vars.color.muted,
|
||||
fontWeight: "bold",
|
||||
fontSize: vars.size.body,
|
||||
});
|
172
src/renderer/src/pages/profile/profile-content.tsx
Normal file
172
src/renderer/src/pages/profile/profile-content.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
import { ProfileHero } from "./profile-hero/profile-hero";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile-content.css";
|
||||
import { ClockIcon, PeopleIcon } from "@primer/octicons-react";
|
||||
|
||||
export function ProfileContent() {
|
||||
const { userProfile } = useContext(userProfileContext);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile) {
|
||||
dispatch(setHeaderTitle(userProfile.displayName));
|
||||
}
|
||||
}, [userProfile, dispatch]);
|
||||
|
||||
const truncatedGamesList = useMemo(() => {
|
||||
return userProfile?.libraryGames.slice(0, 12);
|
||||
}, [userProfile?.libraryGames]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProfileHero />
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
}}
|
||||
>
|
||||
<div style={{}}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>Library</h2>
|
||||
|
||||
<h3>{userProfile?.libraryGames.length}</h3>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
style={{
|
||||
listStyle: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(6, 1fr)",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
{truncatedGamesList.map((game) => (
|
||||
<li
|
||||
key={game.objectId}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}
|
||||
className={styles.game}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{ cursor: "pointer" }}
|
||||
className={styles.gameCover}
|
||||
>
|
||||
<img
|
||||
src={steamUrlBuilder.cover(game.objectId)}
|
||||
alt={game.title}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
minWidth: 350,
|
||||
display: "flex",
|
||||
gap: SPACING_UNIT * 2,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>Played recently</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userProfile?.recentGames.map((game) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<span style={{ fontWeight: "bold" }}>{game.title}</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
}}
|
||||
>
|
||||
<ClockIcon />
|
||||
<span>{game.playTimeInSeconds}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>Friends</h2>
|
||||
|
||||
<span>{userProfile?.totalFriends}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userProfile?.friends.map((friend) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
style={{ cursor: "pointer" }}
|
||||
className={styles.friend}
|
||||
>
|
||||
<img
|
||||
src={friend.profileImageUrl}
|
||||
alt={friend.displayName}
|
||||
style={{ width: "100%" }}
|
||||
className={styles.friendAvatar}
|
||||
/>
|
||||
<span className={styles.friendName}>
|
||||
{friend.displayName}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const profileContentBox = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const profileAvatarContainer = style({
|
||||
width: "96px",
|
||||
minWidth: "96px",
|
||||
height: "96px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
overflow: "hidden",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const profileInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "flex-start",
|
||||
color: "#c0c1c7",
|
||||
zIndex: 1,
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const profileDisplayName = style({
|
||||
fontWeight: "bold",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
});
|
191
src/renderer/src/pages/profile/profile-hero/profile-hero.tsx
Normal file
191
src/renderer/src/pages/profile/profile-hero/profile-hero.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile-hero.css";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import {
|
||||
CheckCircleFillIcon,
|
||||
PersonIcon,
|
||||
XCircleFillIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
|
||||
export function ProfileHero() {
|
||||
const { userProfile, heroBackground, isMe } = useContext(userProfileContext);
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { formatDistance } = useDate();
|
||||
|
||||
if (!userProfile) return null;
|
||||
|
||||
const { currentGame } = userProfile;
|
||||
|
||||
console.log(userProfile);
|
||||
|
||||
const profileActions = useMemo(() => {
|
||||
if (isMe) {
|
||||
return (
|
||||
<>
|
||||
<Button theme="outline">{t("settings")}</Button>
|
||||
|
||||
<Button theme="danger">{t("sign_out")}</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// if (userProfile.relation == null) {
|
||||
// return (
|
||||
// <>
|
||||
// <Button
|
||||
// theme="outline"
|
||||
// onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||
// >
|
||||
// {t("add_friend")}
|
||||
// </Button>
|
||||
|
||||
// <Button theme="danger" onClick={() => setShowUserBlockModal(true)}>
|
||||
// {t("block_user")}
|
||||
// </Button>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (userProfile.relation.status === "ACCEPTED") {
|
||||
// return (
|
||||
// <>
|
||||
// <Button
|
||||
// theme="outline"
|
||||
// // className={styles.cancelRequestButton}
|
||||
// // onClick={() => setShowUndoFriendshipModal(true)}
|
||||
// >
|
||||
// <XCircleFillIcon size={28} /> {t("undo_friendship")}
|
||||
// </Button>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (userProfile.relation.BId === userProfile.id) {
|
||||
// return (
|
||||
// <Button
|
||||
// theme="outline"
|
||||
// // className={styles.cancelRequestButton}
|
||||
// // onClick={() =>
|
||||
// // handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||
// // }
|
||||
// >
|
||||
// <XCircleFillIcon size={28} /> {t("cancel_request")}
|
||||
// </Button>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
// onClick={() =>
|
||||
// handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||
// }
|
||||
>
|
||||
<CheckCircleFillIcon size={28} /> {t("accept_request")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="outline"
|
||||
// onClick={() =>
|
||||
// handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||
// }
|
||||
>
|
||||
<XCircleFillIcon size={28} /> {t("ignore_request")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{ background: heroBackground }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<div className={styles.profileAvatarContainer}>
|
||||
{userProfile.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile.displayName}
|
||||
src={userProfile.profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
{userProfile.displayName}
|
||||
</h2>
|
||||
{currentGame && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Link to={buildGameDetailsPath(currentGame)}>
|
||||
{currentGame.title}
|
||||
</Link>
|
||||
</div>
|
||||
<small>
|
||||
{t("playing_for", {
|
||||
amount: formatDistance(
|
||||
currentGame.sessionDurationInSeconds,
|
||||
new Date()
|
||||
),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "72px",
|
||||
minHeight: "72px",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "space-between",
|
||||
backdropFilter: `blur(10px)`,
|
||||
borderTop: `solid 1px rgba(255, 255, 255, 0.1)`,
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<div></div>
|
||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{profileActions}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import cn from "classnames";
|
||||
import * as styles from "./user.css";
|
||||
import * as styles from "./profile.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const UserSkeleton = () => {
|
||||
export function ProfileSkeleton() {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className={styles.profileHeaderSkeleton} />
|
||||
@ -38,4 +39,4 @@ export const UserSkeleton = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
@ -2,7 +2,6 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const wrapper = style({
|
||||
padding: "24px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
29
src/renderer/src/pages/profile/profile.tsx
Normal file
29
src/renderer/src/pages/profile/profile.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ProfileSkeleton } from "./profile-skeleton";
|
||||
import { ProfileContent } from "./profile-content";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile.css";
|
||||
import {
|
||||
UserProfileContextConsumer,
|
||||
UserProfileContextProvider,
|
||||
} from "@renderer/context";
|
||||
|
||||
export function Profile() {
|
||||
const { userId } = useParams();
|
||||
|
||||
return (
|
||||
<UserProfileContextProvider userId={userId!}>
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<div className={styles.wrapper}>
|
||||
<UserProfileContextConsumer>
|
||||
{({ userProfile }) =>
|
||||
userProfile ? <ProfileContent /> : <ProfileSkeleton />
|
||||
}
|
||||
</UserProfileContextConsumer>
|
||||
</div>
|
||||
</SkeletonTheme>
|
||||
</UserProfileContextProvider>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./user.css";
|
||||
import * as styles from "./profile.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserBlockModalProps {
|
@ -1,5 +1,5 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./user.css";
|
||||
import * as styles from "./profile.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserConfirmUndoFriendshipModalProps {
|
@ -4,7 +4,7 @@ import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { UserProfile } from "@types";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "../user.css";
|
||||
import * as styles from "../profile.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export interface UserEditProfileProps {
|
||||
@ -21,7 +21,7 @@ export const UserEditProfile = ({
|
||||
const [form, setForm] = useState({
|
||||
displayName: userProfile.displayName,
|
||||
profileVisibility: userProfile.profileVisibility,
|
||||
imageProfileUrl: null as string | null,
|
||||
profileImageUrl: null as string | null,
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
@ -55,7 +55,7 @@ export const UserEditProfile = ({
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
|
||||
setForm({ ...form, imageProfileUrl: path });
|
||||
setForm({ ...form, profileImageUrl: path });
|
||||
}
|
||||
};
|
||||
|
||||
@ -86,7 +86,7 @@ export const UserEditProfile = ({
|
||||
};
|
||||
|
||||
const avatarUrl = useMemo(() => {
|
||||
if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`;
|
||||
if (form.profileImageUrl) return `local:${form.profileImageUrl}`;
|
||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||
return null;
|
||||
}, [form, userProfile]);
|
@ -1,5 +1,5 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./user.css";
|
||||
import * as styles from "./profile.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserSignOutModalProps {
|
@ -1,27 +1,106 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const container = style({
|
||||
padding: "24px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "flex-start",
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
backgroundColor: vars.color.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
borderRadius: "4px",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: "1",
|
||||
});
|
||||
|
||||
export const sidebar = style({
|
||||
width: "200px",
|
||||
display: "flex",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
borderRadius: "4px",
|
||||
backgroundColor: vars.color.background,
|
||||
minHeight: "500px",
|
||||
flexDirection: "column",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
});
|
||||
|
||||
export const menuGroup = style({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const settingsCategories = style({
|
||||
export const menu = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const menuItem = recipe({
|
||||
base: {
|
||||
transition: "all ease 0.1s",
|
||||
cursor: "pointer",
|
||||
textWrap: "nowrap",
|
||||
display: "flex",
|
||||
color: vars.color.muted,
|
||||
borderRadius: "4px",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
active: {
|
||||
true: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
},
|
||||
muted: {
|
||||
true: {
|
||||
opacity: vars.opacity.disabled,
|
||||
":hover": {
|
||||
opacity: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const menuItemButton = style({
|
||||
color: "inherit",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
cursor: "pointer",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
padding: `9px ${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const menuItemButtonLabel = style({
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const categoryTitle = style({
|
||||
color: "#ff",
|
||||
fontWeight: "bold",
|
||||
fontSize: "18px",
|
||||
paddingBottom: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Button } from "@renderer/components";
|
||||
|
||||
import * as styles from "./settings.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||
@ -15,12 +13,10 @@ import {
|
||||
export function Settings() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const categories = [
|
||||
t("general"),
|
||||
t("behavior"),
|
||||
t("download_sources"),
|
||||
"Real-Debrid",
|
||||
];
|
||||
const categories = {
|
||||
[t("account")]: [t("my_profile"), t("friends")],
|
||||
Hydra: [t("general"), t("behavior"), t("download_sources"), "Real-Debrid"],
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContextProvider>
|
||||
@ -44,21 +40,34 @@ export function Settings() {
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<section className={styles.settingsCategories}>
|
||||
{categories.map((category, index) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={
|
||||
currentCategoryIndex === index ? "primary" : "outline"
|
||||
}
|
||||
<aside className={styles.sidebar}>
|
||||
{Object.entries(categories).map(([category, items]) => (
|
||||
<div key={category} className={styles.menuGroup}>
|
||||
<span className={styles.categoryTitle}>{category}</span>
|
||||
|
||||
<ul className={styles.menu}>
|
||||
{items.map((item, index) => (
|
||||
<li
|
||||
key={`item-${index}`}
|
||||
className={styles.menuItem({
|
||||
active: currentCategoryIndex === index,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
{item}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</section>
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
<div className={styles.content}>
|
||||
<h2>{categories[currentCategoryIndex]}</h2>
|
||||
{renderCategory()}
|
||||
</div>
|
||||
|
@ -1,581 +0,0 @@
|
||||
import {
|
||||
FriendRequestAction,
|
||||
GameRunning,
|
||||
UserGame,
|
||||
UserProfile,
|
||||
} from "@types";
|
||||
import cn from "classnames";
|
||||
import * as styles from "./user.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import {
|
||||
useAppSelector,
|
||||
useDate,
|
||||
useToast,
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
profileBackgroundFromProfileImage,
|
||||
steamUrlBuilder,
|
||||
} from "@renderer/helpers";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PersonIcon,
|
||||
PlusIcon,
|
||||
TelescopeIcon,
|
||||
XCircleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
import { UserProfileSettingsModal } from "./user-profile-settings-modal";
|
||||
import { UserSignOutModal } from "./user-sign-out-modal";
|
||||
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||
import { UserBlockModal } from "./user-block-modal";
|
||||
import { UserConfirmUndoFriendshipModal } from "./user-confirm-undo-friendship-modal";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
export interface ProfileContentProps {
|
||||
userProfile: UserProfile;
|
||||
updateUserProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
type FriendAction = FriendRequestAction | ("BLOCK" | "UNDO" | "SEND");
|
||||
|
||||
export function UserContent({
|
||||
userProfile,
|
||||
updateUserProfile,
|
||||
}: ProfileContentProps) {
|
||||
const { t, i18n } = useTranslation("user_profile");
|
||||
const {
|
||||
userDetails,
|
||||
profileBackground,
|
||||
signOut,
|
||||
sendFriendRequest,
|
||||
fetchFriendRequests,
|
||||
showFriendsModal,
|
||||
updateFriendRequestState,
|
||||
undoFriendship,
|
||||
blockUser,
|
||||
} = useUserDetails();
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
||||
useState<string | undefined>();
|
||||
const [showProfileSettingsModal, setShowProfileSettingsModal] =
|
||||
useState(false);
|
||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
||||
const [showUndoFriendshipModal, setShowUndoFriendshipModal] = useState(false);
|
||||
const [currentGame, setCurrentGame] = useState<GameRunning | null>(null);
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const numberFormatter = useMemo(() => {
|
||||
return new Intl.NumberFormat(i18n.language, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}, [i18n.language]);
|
||||
|
||||
const { formatDistance, formatDiffInMillis } = useDate();
|
||||
|
||||
const formatPlayTime = () => {
|
||||
const seconds = userProfile.totalPlayTimeInSeconds;
|
||||
const minutes = seconds / 60;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes", {
|
||||
amount: minutes.toFixed(0),
|
||||
});
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
};
|
||||
|
||||
const handleGameClick = (game: UserGame) => {
|
||||
navigate(buildGameDetailsPath(game));
|
||||
};
|
||||
|
||||
const handleEditProfile = () => {
|
||||
setShowProfileSettingsModal(true);
|
||||
};
|
||||
|
||||
const handleOnClickFriend = (userId: string) => {
|
||||
navigate(`/user/${userId}`);
|
||||
};
|
||||
|
||||
const handleConfirmSignout = async () => {
|
||||
await signOut();
|
||||
|
||||
showSuccessToast(t("successfully_signed_out"));
|
||||
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const isMe = userDetails?.id == userProfile.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (isMe && gameRunning) {
|
||||
setCurrentGame(gameRunning);
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentGame(userProfile.currentGame);
|
||||
}, [gameRunning, isMe, userProfile.currentGame]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMe) fetchFriendRequests();
|
||||
}, [isMe, fetchFriendRequests]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMe && profileBackground) {
|
||||
setProfileContentBoxBackground(profileBackground);
|
||||
}
|
||||
|
||||
if (userProfile.profileImageUrl) {
|
||||
profileBackgroundFromProfileImage(userProfile.profileImageUrl).then(
|
||||
(profileBackground) => {
|
||||
setProfileContentBoxBackground(profileBackground);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [profileBackground, isMe, userProfile.profileImageUrl]);
|
||||
|
||||
const handleFriendAction = (userId: string, action: FriendAction) => {
|
||||
try {
|
||||
if (action === "UNDO") {
|
||||
undoFriendship(userId).then(updateUserProfile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "BLOCK") {
|
||||
blockUser(userId).then(() => {
|
||||
setShowUserBlockModal(false);
|
||||
showSuccessToast(t("user_blocked_successfully"));
|
||||
navigate(-1);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "SEND") {
|
||||
sendFriendRequest(userProfile.id).then(updateUserProfile);
|
||||
return;
|
||||
}
|
||||
|
||||
updateFriendRequestState(userId, action).then(updateUserProfile);
|
||||
} catch (err) {
|
||||
showErrorToast(t("try_again"));
|
||||
}
|
||||
};
|
||||
|
||||
const showFriends = isMe || userProfile.totalFriends > 0;
|
||||
const showProfileContent =
|
||||
isMe ||
|
||||
userProfile.profileVisibility === "PUBLIC" ||
|
||||
(userProfile.relation?.status === "ACCEPTED" &&
|
||||
userProfile.profileVisibility === "FRIENDS");
|
||||
|
||||
const getProfileActions = () => {
|
||||
if (isMe) {
|
||||
return (
|
||||
<>
|
||||
<Button theme="outline" onClick={handleEditProfile}>
|
||||
{t("settings")}
|
||||
</Button>
|
||||
|
||||
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
||||
{t("sign_out")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (userProfile.relation == null) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||
>
|
||||
{t("add_friend")}
|
||||
</Button>
|
||||
|
||||
<Button theme="danger" onClick={() => setShowUserBlockModal(true)}>
|
||||
{t("block_user")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (userProfile.relation.status === "ACCEPTED") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() => setShowUndoFriendshipModal(true)}
|
||||
>
|
||||
<XCircleIcon size={28} /> {t("undo_friendship")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (userProfile.relation.BId === userProfile.id) {
|
||||
return (
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||
}
|
||||
>
|
||||
<XCircleIcon size={28} /> {t("cancel_request")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.acceptRequestButton}
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||
}
|
||||
>
|
||||
<CheckCircleIcon size={28} /> {t("accept_request")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||
}
|
||||
>
|
||||
<XCircleIcon size={28} /> {t("ignore_request")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserProfileSettingsModal
|
||||
visible={showProfileSettingsModal}
|
||||
onClose={() => setShowProfileSettingsModal(false)}
|
||||
updateUserProfile={updateUserProfile}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
|
||||
<UserSignOutModal
|
||||
visible={showSignOutModal}
|
||||
onClose={() => setShowSignOutModal(false)}
|
||||
onConfirm={handleConfirmSignout}
|
||||
/>
|
||||
|
||||
<UserBlockModal
|
||||
visible={showUserBlockModal}
|
||||
onClose={() => setShowUserBlockModal(false)}
|
||||
onConfirm={() => handleFriendAction(userProfile.id, "BLOCK")}
|
||||
displayName={userProfile.displayName}
|
||||
/>
|
||||
|
||||
<UserConfirmUndoFriendshipModal
|
||||
visible={showUndoFriendshipModal}
|
||||
onClose={() => setShowUndoFriendshipModal(false)}
|
||||
onConfirm={() => handleFriendAction(userProfile.id, "UNDO")}
|
||||
displayName={userProfile.displayName}
|
||||
/>
|
||||
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{currentGame && (
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(currentGame.objectID)}
|
||||
alt={currentGame.title}
|
||||
className={styles.profileBackground}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: profileContentBoxBackground,
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className={styles.profileAvatarContainer}>
|
||||
{userProfile.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile.displayName}
|
||||
src={userProfile.profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
{userProfile.displayName}
|
||||
</h2>
|
||||
{currentGame && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Link to={buildGameDetailsPath(currentGame)}>
|
||||
{currentGame.title}
|
||||
</Link>
|
||||
</div>
|
||||
<small>
|
||||
{t("playing_for", {
|
||||
amount: formatDiffInMillis(
|
||||
currentGame.sessionDurationInMillis,
|
||||
new Date()
|
||||
),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
justifyContent: "end",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{getProfileActions()}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showProfileContent && (
|
||||
<div className={styles.profileContent}>
|
||||
<div className={styles.profileGameSection}>
|
||||
<h2>{t("activity")}</h2>
|
||||
|
||||
{!userProfile.recentGames.length ? (
|
||||
<div className={styles.noDownloads}>
|
||||
<div className={styles.telescopeIcon}>
|
||||
<TelescopeIcon size={24} />
|
||||
</div>
|
||||
<h2>{t("no_recent_activity_title")}</h2>
|
||||
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
{userProfile.recentGames.map((game) => (
|
||||
<button
|
||||
key={game.objectID}
|
||||
className={cn(styles.feedItem, styles.profileContentBox)}
|
||||
onClick={() => handleGameClick(game)}
|
||||
>
|
||||
<img
|
||||
className={styles.feedGameIcon}
|
||||
src={game.cover}
|
||||
alt={game.title}
|
||||
/>
|
||||
<div className={styles.gameInformation}>
|
||||
<h4>{game.title}</h4>
|
||||
<small>
|
||||
{t("last_time_played", {
|
||||
period: formatDistance(
|
||||
game.lastTimePlayed!,
|
||||
new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
}
|
||||
),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.contentSidebar}>
|
||||
<div className={styles.profileGameSection}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<h2>{t("library")}</h2>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: vars.color.border,
|
||||
height: "1px",
|
||||
}}
|
||||
/>
|
||||
<h3 style={{ fontWeight: "400" }}>
|
||||
{userProfile.libraryGames.length}
|
||||
</h3>
|
||||
</div>
|
||||
<small>
|
||||
{t("total_play_time", { amount: formatPlayTime() })}
|
||||
</small>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{userProfile.libraryGames.map((game) => (
|
||||
<button
|
||||
key={game.objectID}
|
||||
className={cn(
|
||||
styles.gameListItem,
|
||||
styles.profileContentBox
|
||||
)}
|
||||
onClick={() => handleGameClick(game)}
|
||||
title={game.title}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className={styles.libraryGameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.libraryGameIcon} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showFriends && (
|
||||
<div className={styles.friendsSection}>
|
||||
<button
|
||||
className={styles.friendsSectionHeader}
|
||||
onClick={() =>
|
||||
showFriendsModal(
|
||||
UserFriendModalTab.FriendsList,
|
||||
userProfile.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<h2>{t("friends")}</h2>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: vars.color.border,
|
||||
height: "1px",
|
||||
}}
|
||||
/>
|
||||
<h3 style={{ fontWeight: "400" }}>
|
||||
{userProfile.totalFriends}
|
||||
</h3>
|
||||
</button>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{userProfile.friends.map((friend) => {
|
||||
return (
|
||||
<button
|
||||
key={friend.id}
|
||||
className={cn(
|
||||
styles.profileContentBox,
|
||||
styles.friendListContainer
|
||||
)}
|
||||
onClick={() => handleOnClickFriend(friend.id)}
|
||||
>
|
||||
<div className={styles.friendAvatarContainer}>
|
||||
{friend.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.friendProfileIcon}
|
||||
src={friend.profileImageUrl}
|
||||
alt={friend.displayName}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className={styles.friendListDisplayName}>
|
||||
{friend.displayName}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{isMe && (
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() =>
|
||||
showFriendsModal(
|
||||
UserFriendModalTab.AddFriend,
|
||||
userProfile.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<PlusIcon /> {t("add")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { UserProfile } from "@types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { useAppDispatch, useToast } from "@renderer/hooks";
|
||||
import { UserSkeleton } from "./user-skeleton";
|
||||
import { UserContent } from "./user-content";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import * as styles from "./user.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const User = () => {
|
||||
const { userId } = useParams();
|
||||
const [userProfile, setUserProfile] = useState<UserProfile>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const { showErrorToast } = useToast();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getUserProfile = useCallback(() => {
|
||||
return window.electron.getUser(userId!).then((userProfile) => {
|
||||
if (userProfile) {
|
||||
dispatch(setHeaderTitle(userProfile.displayName));
|
||||
setUserProfile(userProfile);
|
||||
} else {
|
||||
showErrorToast(t("user_not_found"));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}, [dispatch, navigate, showErrorToast, userId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
getUserProfile();
|
||||
}, [getUserProfile]);
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<div className={styles.wrapper}>
|
||||
{userProfile ? (
|
||||
<UserContent
|
||||
userProfile={userProfile}
|
||||
updateUserProfile={getUserProfile}
|
||||
/>
|
||||
) : (
|
||||
<UserSkeleton />
|
||||
)}
|
||||
</div>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
@ -98,3 +98,16 @@ export const getDownloadersForUris = (uris: string[]) => {
|
||||
|
||||
return Array.from(downloadersSet);
|
||||
};
|
||||
|
||||
export const steamUrlBuilder = {
|
||||
library: (objectID: string) =>
|
||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
|
||||
libraryHero: (objectID: string) =>
|
||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/library_hero.jpg`,
|
||||
logo: (objectID: string) =>
|
||||
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
|
||||
cover: (objectID: string) =>
|
||||
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/library_600x900.jpg`,
|
||||
icon: (objectID: string, clientIcon: string) =>
|
||||
`https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${objectID}/${clientIcon}.ico`,
|
||||
};
|
||||
|
@ -99,7 +99,7 @@ export interface CatalogueEntry {
|
||||
}
|
||||
|
||||
export interface UserGame {
|
||||
objectID: string;
|
||||
objectId: string;
|
||||
shop: GameShop;
|
||||
title: string;
|
||||
iconUrl: string | null;
|
||||
@ -219,7 +219,7 @@ export interface RealDebridUnrestrictLink {
|
||||
|
||||
export interface RealDebridAddMagnet {
|
||||
id: string;
|
||||
// URL of the created ressource
|
||||
// URL of the created resource
|
||||
uri: string;
|
||||
}
|
||||
|
||||
@ -280,6 +280,8 @@ export interface UserFriend {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserFriends {
|
||||
@ -307,6 +309,10 @@ export interface UserRelation {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserProfileCurrentGame extends GameRunning {
|
||||
sessionDurationInSeconds: number;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
displayName: string;
|
||||
@ -318,10 +324,10 @@ export interface UserProfile {
|
||||
friends: UserFriend[];
|
||||
totalFriends: number;
|
||||
relation: UserRelation | null;
|
||||
currentGame: GameRunning | null;
|
||||
currentGame: UserProfileCurrentGame | null;
|
||||
}
|
||||
|
||||
export interface UpdateProfileProps {
|
||||
export interface UpdateProfileRequest {
|
||||
displayName?: string;
|
||||
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||
profileImageUrl?: string | null;
|
||||
|
98
yarn.lock
98
yarn.lock
@ -579,6 +579,13 @@
|
||||
"@types/conventional-commits-parser" "^5.0.0"
|
||||
chalk "^5.3.0"
|
||||
|
||||
"@cspotcode/source-map-support@^0.8.0":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
|
||||
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@develar/schema-utils@~2.6.5":
|
||||
version "2.6.5"
|
||||
resolved "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz"
|
||||
@ -1003,7 +1010,7 @@
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.1.0":
|
||||
"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
|
||||
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
||||
@ -1018,6 +1025,14 @@
|
||||
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
|
||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||
|
||||
"@jridgewell/trace-mapping@0.3.9":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
|
||||
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
|
||||
version "0.3.25"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz"
|
||||
@ -1876,6 +1891,26 @@
|
||||
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
|
||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
|
||||
integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==
|
||||
|
||||
"@tsconfig/node12@^1.0.7":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
|
||||
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
|
||||
|
||||
"@tsconfig/node14@^1.0.0":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
|
||||
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
|
||||
|
||||
"@tsconfig/node16@^1.0.2":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||
|
||||
"@types/accepts@*":
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
|
||||
@ -2521,6 +2556,18 @@ acorn-jsx@^5.3.2:
|
||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.3.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e"
|
||||
integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==
|
||||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.11.0, acorn@^8.4.1:
|
||||
version "8.12.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
|
||||
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
|
||||
|
||||
acorn@^8.11.3, acorn@^8.9.0:
|
||||
version "8.11.3"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
|
||||
@ -2660,6 +2707,11 @@ applescript@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/applescript/-/applescript-1.0.0.tgz"
|
||||
integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
|
||||
@ -3373,6 +3425,11 @@ create-desktop-shortcuts@^1.11.0:
|
||||
dependencies:
|
||||
which "2.0.2"
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
cross-fetch-ponyfill@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz"
|
||||
@ -3576,6 +3633,11 @@ detect-node@^2.0.4:
|
||||
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
|
||||
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
dir-compare@^3.0.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz"
|
||||
@ -5617,6 +5679,11 @@ magnet-uri@^7.0.5:
|
||||
bep53-range "^2.0.0"
|
||||
uint8-util "^2.1.9"
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
matcher@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz"
|
||||
@ -7273,6 +7340,25 @@ ts-api-utils@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz"
|
||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
||||
|
||||
ts-node@^10.9.2:
|
||||
version "10.9.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
||||
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support" "^0.8.0"
|
||||
"@tsconfig/node10" "^1.0.7"
|
||||
"@tsconfig/node12" "^1.0.7"
|
||||
"@tsconfig/node14" "^1.0.0"
|
||||
"@tsconfig/node16" "^1.0.2"
|
||||
acorn "^8.4.1"
|
||||
acorn-walk "^8.1.1"
|
||||
arg "^4.1.0"
|
||||
create-require "^1.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
|
||||
tslib@^2.0.3, tslib@^2.5.0, tslib@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
||||
@ -7484,6 +7570,11 @@ uuid@^9.0.0:
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||
|
||||
verror@^1.10.0:
|
||||
version "1.10.1"
|
||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb"
|
||||
@ -7777,6 +7868,11 @@ yauzl@^2.10.0:
|
||||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user