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 type { CatalogueEntry, GameShop } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { RepacksManager, requestSteam250 } from "@main/services";
|
import { RepacksManager, requestSteam250 } from "@main/services";
|
||||||
import { formatName } from "@shared";
|
import { formatName, steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
const resultSize = 12;
|
const resultSize = 12;
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ const getCatalogue = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
objectID,
|
objectID,
|
||||||
title,
|
title,
|
||||||
shop: "steam" as GameShop,
|
shop: "steam" as GameShop,
|
||||||
cover: getSteamAppAsset("library", objectID),
|
cover: steamUrlBuilder.library(objectID),
|
||||||
};
|
};
|
||||||
|
|
||||||
results.push({ ...catalogueEntry, repacks });
|
results.push({ ...catalogueEntry, repacks });
|
||||||
|
@ -3,9 +3,9 @@ import flexSearch from "flexsearch";
|
|||||||
|
|
||||||
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
||||||
|
|
||||||
import { getSteamAppAsset } from "@main/helpers";
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { RepacksManager } from "@main/services";
|
import { RepacksManager } from "@main/services";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
export interface SearchGamesArgs {
|
export interface SearchGamesArgs {
|
||||||
query?: string;
|
query?: string;
|
||||||
@ -19,7 +19,7 @@ export const convertSteamGameToCatalogueEntry = (
|
|||||||
objectID: String(game.id),
|
objectID: String(game.id),
|
||||||
title: game.name,
|
title: game.name,
|
||||||
shop: "steam" as GameShop,
|
shop: "steam" as GameShop,
|
||||||
cover: getSteamAppAsset("library", String(game.id)),
|
cover: steamUrlBuilder.library(String(game.id)),
|
||||||
repacks: [],
|
repacks: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,10 +3,11 @@ import { gameRepository } from "@main/repository";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
import { getFileBase64 } from "@main/helpers";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -32,7 +33,7 @@ const addGameToLibrary = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
const iconUrl = steamGame?.clientIcon
|
||||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await gameRepository
|
await gameRepository
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi, logger } from "@main/services";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileTypeFromFile } from "file-type";
|
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);
|
return HydraApi.patch("/profile", updateProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProfile = async (
|
const updateProfile = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
updateProfile: UpdateProfileProps
|
updateProfile: UpdateProfileRequest
|
||||||
): Promise<UserProfile> => {
|
): Promise<UserProfile> => {
|
||||||
if (!updateProfile.profileImageUrl) {
|
if (!updateProfile.profileImageUrl) {
|
||||||
return patchUserProfile(updateProfile);
|
return patchUserProfile(updateProfile);
|
||||||
@ -40,7 +40,11 @@ const updateProfile = async (
|
|||||||
});
|
});
|
||||||
return profileImageUrl as string;
|
return profileImageUrl as string;
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch((err) => {
|
||||||
|
logger.error("Error uploading profile image", err);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||||
};
|
};
|
||||||
|
@ -7,12 +7,13 @@ import {
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
import { getFileBase64 } from "@main/helpers";
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
|
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -65,7 +66,7 @@ const startGameDownload = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
const iconUrl = steamGame?.clientIcon
|
||||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await gameRepository
|
await gameRepository
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { GameRunning, UserGame, UserProfile } from "@types";
|
import type { UserProfile } from "@types";
|
||||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
|
||||||
import { getSteamAppAsset } from "@main/helpers";
|
|
||||||
import { getUserFriends } from "./get-user-friends";
|
import { getUserFriends } from "./get-user-friends";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
const getUser = async (
|
const getUser = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -12,65 +11,47 @@ const getUser = async (
|
|||||||
): Promise<UserProfile | null> => {
|
): Promise<UserProfile | null> => {
|
||||||
try {
|
try {
|
||||||
const [profile, friends] = await Promise.all([
|
const [profile, friends] = await Promise.all([
|
||||||
HydraApi.get(`/users/${userId}`),
|
HydraApi.get<UserProfile | null>(`/users/${userId}`),
|
||||||
getUserFriends(userId, 12, 0).catch(() => {
|
getUserFriends(userId, 12, 0).catch(() => {
|
||||||
return { totalFriends: 0, friends: [] };
|
return { totalFriends: 0, friends: [] };
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (!profile) return null;
|
||||||
|
|
||||||
const recentGames = await Promise.all(
|
const recentGames = await Promise.all(
|
||||||
profile.recentGames.map(async (game) => {
|
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(
|
// const libraryGames = await Promise.all(
|
||||||
profile.libraryGames.map(async (game) => {
|
// profile.libraryGames.map(async (game) => {
|
||||||
return getSteamUserGame(game);
|
// return getSteamUserGame(game);
|
||||||
})
|
// })
|
||||||
);
|
// );
|
||||||
|
|
||||||
const currentGame = await getGameRunning(profile.currentGame);
|
// const currentGame = await getGameRunning(profile.currentGame);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...profile,
|
...profile,
|
||||||
libraryGames,
|
// libraryGames,
|
||||||
recentGames,
|
recentGames,
|
||||||
friends: friends.friends,
|
friends: friends.friends,
|
||||||
totalFriends: friends.totalFriends,
|
totalFriends: friends.totalFriends,
|
||||||
currentGame,
|
// currentGame,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
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);
|
registerEvent("getUser", getUser);
|
||||||
|
@ -2,23 +2,6 @@ import axios from "axios";
|
|||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import UserAgent from "user-agents";
|
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) =>
|
export const getFileBuffer = async (url: string) =>
|
||||||
fetch(url, { method: "GET" }).then((response) =>
|
fetch(url, { method: "GET" }).then((response) =>
|
||||||
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
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) =>
|
export const sleep = (ms: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
@ -77,54 +77,54 @@ export class HydraApi {
|
|||||||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance.interceptors.request.use(
|
// this.instance.interceptors.request.use(
|
||||||
(request) => {
|
// (request) => {
|
||||||
logger.log(" ---- REQUEST -----");
|
// logger.log(" ---- REQUEST -----");
|
||||||
logger.log(request.method, request.url, request.params, request.data);
|
// logger.log(request.method, request.url, request.params, request.data);
|
||||||
return request;
|
// return request;
|
||||||
},
|
// },
|
||||||
(error) => {
|
// (error) => {
|
||||||
logger.error("request error", error);
|
// logger.error("request error", error);
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
this.instance.interceptors.response.use(
|
// this.instance.interceptors.response.use(
|
||||||
(response) => {
|
// (response) => {
|
||||||
logger.log(" ---- RESPONSE -----");
|
// logger.log(" ---- RESPONSE -----");
|
||||||
logger.log(
|
// logger.log(
|
||||||
response.status,
|
// response.status,
|
||||||
response.config.method,
|
// response.config.method,
|
||||||
response.config.url,
|
// response.config.url,
|
||||||
response.data
|
// response.data
|
||||||
);
|
// );
|
||||||
return response;
|
// return response;
|
||||||
},
|
// },
|
||||||
(error) => {
|
// (error) => {
|
||||||
logger.error(" ---- RESPONSE ERROR -----");
|
// logger.error(" ---- RESPONSE ERROR -----");
|
||||||
|
|
||||||
const { config } = error;
|
// const { config } = error;
|
||||||
|
|
||||||
logger.error(
|
// logger.error(
|
||||||
config.method,
|
// config.method,
|
||||||
config.baseURL,
|
// config.baseURL,
|
||||||
config.url,
|
// config.url,
|
||||||
config.headers,
|
// config.headers,
|
||||||
config.data
|
// config.data
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (error.response) {
|
// if (error.response) {
|
||||||
logger.error("Response", error.response.status, error.response.data);
|
// logger.error("Response", error.response.status, error.response.data);
|
||||||
} else if (error.request) {
|
// } else if (error.request) {
|
||||||
logger.error("Request", error.request);
|
// logger.error("Request", error.request);
|
||||||
} else {
|
// } else {
|
||||||
logger.error("Error", error.message);
|
// logger.error("Error", error.message);
|
||||||
}
|
// }
|
||||||
|
|
||||||
logger.error(" ----- END RESPONSE ERROR -------");
|
// logger.error(" ----- END RESPONSE ERROR -------");
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
const userAuth = await userAuthRepository.findOne({
|
const userAuth = await userAuthRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { getSteamAppAsset } from "@main/helpers";
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
return HydraApi.get("/profile/games")
|
return HydraApi.get("/profile/games")
|
||||||
@ -44,7 +44,7 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
|
|
||||||
if (steamGame) {
|
if (steamGame) {
|
||||||
const iconUrl = steamGame?.clientIcon
|
const iconUrl = steamGame?.clientIcon
|
||||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
gameRepository.insert({
|
gameRepository.insert({
|
||||||
|
@ -10,7 +10,7 @@ import type {
|
|||||||
StartGameDownloadPayload,
|
StartGameDownloadPayload,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UpdateProfileProps,
|
UpdateProfileRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
@ -138,7 +138,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
undoFriendship: (userId: string) =>
|
undoFriendship: (userId: string) =>
|
||||||
ipcRenderer.invoke("undoFriendship", userId),
|
ipcRenderer.invoke("undoFriendship", userId),
|
||||||
updateProfile: (updateProfile: UpdateProfileProps) =>
|
updateProfile: (updateProfile: UpdateProfileRequest) =>
|
||||||
ipcRenderer.invoke("updateProfile", updateProfile),
|
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
|
@ -39,7 +39,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (location.pathname.startsWith("/game")) return headerTitle;
|
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");
|
if (location.pathname.startsWith("/search")) return t("search_results");
|
||||||
|
|
||||||
return t(pathTitle[location.pathname]);
|
return t(pathTitle[location.pathname]);
|
||||||
|
@ -2,12 +2,9 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import * as styles from "./hero.css";
|
import * as styles from "./hero.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ShopDetails } from "@types";
|
import { ShopDetails } from "@types";
|
||||||
import {
|
import { buildGameDetailsPath, getSteamLanguage } from "@renderer/helpers";
|
||||||
buildGameDetailsPath,
|
|
||||||
getSteamLanguage,
|
|
||||||
steamUrlBuilder,
|
|
||||||
} from "@renderer/helpers";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
const FEATURED_GAME_TITLE = "ELDEN RING";
|
const FEATURED_GAME_TITLE = "ELDEN RING";
|
||||||
const FEATURED_GAME_ID = "1245620";
|
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";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const profileContainerBackground = createVar();
|
|
||||||
|
|
||||||
export const profileContainer = style({
|
export const profileContainer = style({
|
||||||
background: profileContainerBackground,
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
cursor: "pointer",
|
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||||
":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`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileButton = style({
|
export const profileButton = style({
|
||||||
@ -25,13 +17,17 @@ export const profileButton = style({
|
|||||||
color: vars.color.muted,
|
color: vars.color.muted,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileButtonContent = style({
|
export const profileButtonContent = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||||
height: "40px",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,14 +73,31 @@ export const profileButtonTitle = style({
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const friendRequestButton = style({
|
export const friendsButton = style({
|
||||||
color: vars.color.success,
|
color: vars.color.muted,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
overflow: "hidden",
|
|
||||||
width: "40px",
|
width: "40px",
|
||||||
|
minWidth: "40px",
|
||||||
|
minHeight: "40px",
|
||||||
height: "40px",
|
height: "40px",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
position: "relative",
|
||||||
|
transition: "all ease 0.3s",
|
||||||
":hover": {
|
":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 { 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 * as styles from "./sidebar-profile.css";
|
||||||
import { assignInlineVars } from "@vanilla-extract/dynamic";
|
|
||||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { profileContainerBackground } from "./sidebar-profile.css";
|
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
import { FriendRequest } from "@types";
|
import { FriendRequest } from "@types";
|
||||||
|
|
||||||
@ -14,8 +12,7 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
|
|
||||||
const { userDetails, profileBackground, friendRequests, showFriendsModal } =
|
const { userDetails, friendRequests, showFriendsModal } = useUserDetails();
|
||||||
useUserDetails();
|
|
||||||
|
|
||||||
const [receivedRequests, setReceivedRequests] = useState<FriendRequest[]>([]);
|
const [receivedRequests, setReceivedRequests] = useState<FriendRequest[]>([]);
|
||||||
|
|
||||||
@ -33,24 +30,11 @@ export function SidebarProfile() {
|
|||||||
return;
|
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 (
|
return (
|
||||||
<div
|
<div className={styles.profileContainer}>
|
||||||
className={styles.profileContainer}
|
|
||||||
style={assignInlineVars({
|
|
||||||
[profileContainerBackground]: profileButtonBackground,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.profileButton}
|
className={styles.profileButton}
|
||||||
@ -91,18 +75,18 @@ export function SidebarProfile() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{showPendingRequests && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendRequestButton}
|
className={styles.friendsButton}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PersonAddIcon size={24} />
|
<small className={styles.friendsButtonLabel}>10</small>
|
||||||
{receivedRequests.length}
|
|
||||||
</button>
|
<PeopleIcon size={16} />
|
||||||
)}
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,22 +24,13 @@ export const sidebar = recipe({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = recipe({
|
export const content = style({
|
||||||
base: {
|
display: "flex",
|
||||||
display: "flex",
|
flexDirection: "column",
|
||||||
flexDirection: "column",
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
width: "100%",
|
||||||
width: "100%",
|
overflow: "auto",
|
||||||
overflow: "auto",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
macos: {
|
|
||||||
true: {
|
|
||||||
paddingTop: `${SPACING_UNIT * 6}px`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const handle = style({
|
export const handle = style({
|
||||||
|
@ -15,6 +15,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
|||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
import { ChevronDownIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@ -157,17 +158,12 @@ export function Sidebar() {
|
|||||||
width: sidebarWidth,
|
width: sidebarWidth,
|
||||||
minWidth: sidebarWidth,
|
minWidth: sidebarWidth,
|
||||||
maxWidth: sidebarWidth,
|
maxWidth: sidebarWidth,
|
||||||
|
paddingTop: 8 * 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SidebarProfile />
|
<SidebarProfile />
|
||||||
|
|
||||||
<div
|
<div className={styles.content}>
|
||||||
className={styles.content({
|
|
||||||
macos: window.electron.platform === "darwin",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{window.electron.platform === "darwin" && <h2>Hydra</h2>}
|
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<ul className={styles.menu}>
|
<ul className={styles.menu}>
|
||||||
{routes.map(({ nameKey, path, render }) => (
|
{routes.map(({ nameKey, path, render }) => (
|
||||||
@ -184,6 +180,8 @@ export function Sidebar() {
|
|||||||
>
|
>
|
||||||
{render(isDownloading)}
|
{render(isDownloading)}
|
||||||
<span>{t(nameKey)}</span>
|
<span>{t(nameKey)}</span>
|
||||||
|
|
||||||
|
<ChevronDownIcon />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from "./game-details/game-details.context";
|
export * from "./game-details/game-details.context";
|
||||||
export * from "./settings/settings.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,
|
FriendRequestAction,
|
||||||
UserFriends,
|
UserFriends,
|
||||||
UserBlocks,
|
UserBlocks,
|
||||||
|
UpdateProfileRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@ -141,7 +142,9 @@ declare global {
|
|||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
undoFriendship: (userId: string) => Promise<void>;
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
updateProfile: (
|
||||||
|
updateProfile: UpdateProfileRequest
|
||||||
|
) => Promise<UserProfile>;
|
||||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
updateFriendRequest: (
|
updateFriendRequest: (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -1,16 +1,6 @@
|
|||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import Color from "color";
|
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) => {
|
export const formatDownloadProgress = (progress?: number) => {
|
||||||
if (!progress) return "0%";
|
if (!progress) return "0%";
|
||||||
@ -46,14 +36,3 @@ export const buildGameDetailsPath = (
|
|||||||
|
|
||||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||||
new Color(color).darken(amount).alpha(alpha).toString();
|
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,
|
setFriendsModalVisible,
|
||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
// import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
||||||
import { FriendRequestAction, UpdateProfileProps, UserDetails } from "@types";
|
import type {
|
||||||
|
FriendRequestAction,
|
||||||
|
UpdateProfileRequest,
|
||||||
|
UserDetails,
|
||||||
|
} from "@types";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
import { logger } from "@renderer/logger";
|
import { logger } from "@renderer/logger";
|
||||||
|
|
||||||
@ -42,12 +46,12 @@ export function useUserDetails() {
|
|||||||
dispatch(setUserDetails(userDetails));
|
dispatch(setUserDetails(userDetails));
|
||||||
|
|
||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
const profileBackground = await profileBackgroundFromProfileImage(
|
// const profileBackground = await profileBackgroundFromProfileImage(
|
||||||
userDetails.profileImageUrl
|
// userDetails.profileImageUrl
|
||||||
).catch((err) => {
|
// ).catch((err) => {
|
||||||
logger.error("profileBackgroundFromProfileImage", err);
|
// logger.error("profileBackgroundFromProfileImage", err);
|
||||||
return `#151515B3`;
|
// return `#151515B3`;
|
||||||
});
|
// });
|
||||||
dispatch(setProfileBackground(profileBackground));
|
dispatch(setProfileBackground(profileBackground));
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@ -78,8 +82,9 @@ export function useUserDetails() {
|
|||||||
}, [clearUserDetails]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (props: UpdateProfileProps) => {
|
async (values: UpdateProfileRequest) => {
|
||||||
const response = await window.electron.updateProfile(props);
|
console.log("values", values);
|
||||||
|
const response = await window.electron.updateProfile(values);
|
||||||
return updateUserDetails(response);
|
return updateUserDetails(response);
|
||||||
},
|
},
|
||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
|
@ -22,12 +22,12 @@ import {
|
|||||||
SearchResults,
|
SearchResults,
|
||||||
Settings,
|
Settings,
|
||||||
Catalogue,
|
Catalogue,
|
||||||
|
Profile,
|
||||||
} from "@renderer/pages";
|
} from "@renderer/pages";
|
||||||
|
|
||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
|
||||||
import * as resources from "@locales";
|
import * as resources from "@locales";
|
||||||
import { User } from "./pages/user/user";
|
|
||||||
|
|
||||||
Sentry.init({});
|
Sentry.init({});
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||||
<Route path="/search" Component={SearchResults} />
|
<Route path="/search" Component={SearchResults} />
|
||||||
<Route path="/settings" Component={Settings} />
|
<Route path="/settings" Component={Settings} />
|
||||||
<Route path="/user/:userId" Component={User} />
|
<Route path="/profile/:userId" Component={Profile} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
@ -6,10 +6,9 @@ import { Badge, Button } from "@renderer/components";
|
|||||||
import {
|
import {
|
||||||
buildGameDetailsPath,
|
buildGameDetailsPath,
|
||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
steamUrlBuilder,
|
|
||||||
} from "@renderer/helpers";
|
} from "@renderer/helpers";
|
||||||
|
|
||||||
import { Downloader, formatBytes } from "@shared";
|
import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ import { useContext, useEffect, useRef, useState } from "react";
|
|||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
|
|
||||||
import { steamUrlBuilder } from "@renderer/helpers";
|
|
||||||
|
|
||||||
import { HeroPanel } from "./hero";
|
import { HeroPanel } from "./hero";
|
||||||
import { DescriptionHeader } from "./description-header/description-header";
|
import { DescriptionHeader } from "./description-header/description-header";
|
||||||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||||
@ -12,6 +10,7 @@ import { Sidebar } from "./sidebar/sidebar";
|
|||||||
import * as styles from "./game-details.css";
|
import * as styles from "./game-details.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
const HERO_ANIMATION_THRESHOLD = 25;
|
const HERO_ANIMATION_THRESHOLD = 25;
|
||||||
|
|
||||||
|
@ -4,3 +4,4 @@ export * from "./downloads/downloads";
|
|||||||
export * from "./home/search-results";
|
export * from "./home/search-results";
|
||||||
export * from "./settings/settings";
|
export * from "./settings/settings";
|
||||||
export * from "./catalogue/catalogue";
|
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 Skeleton from "react-loading-skeleton";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./profile.css";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const UserSkeleton = () => {
|
export function ProfileSkeleton() {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Skeleton className={styles.profileHeaderSkeleton} />
|
<Skeleton className={styles.profileHeaderSkeleton} />
|
||||||
@ -38,4 +39,4 @@ export const UserSkeleton = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
@ -2,7 +2,6 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const wrapper = style({
|
export const wrapper = style({
|
||||||
padding: "24px",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
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 { Button, Modal } from "@renderer/components";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./profile.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface UserBlockModalProps {
|
export interface UserBlockModalProps {
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./profile.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface UserConfirmUndoFriendshipModalProps {
|
export interface UserConfirmUndoFriendshipModalProps {
|
@ -4,7 +4,7 @@ import { useToast, useUserDetails } from "@renderer/hooks";
|
|||||||
import { UserProfile } from "@types";
|
import { UserProfile } from "@types";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "../user.css";
|
import * as styles from "../profile.css";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
|
||||||
export interface UserEditProfileProps {
|
export interface UserEditProfileProps {
|
||||||
@ -21,7 +21,7 @@ export const UserEditProfile = ({
|
|||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
displayName: userProfile.displayName,
|
displayName: userProfile.displayName,
|
||||||
profileVisibility: userProfile.profileVisibility,
|
profileVisibility: userProfile.profileVisibility,
|
||||||
imageProfileUrl: null as string | null,
|
profileImageUrl: null as string | null,
|
||||||
});
|
});
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export const UserEditProfile = ({
|
|||||||
if (filePaths && filePaths.length > 0) {
|
if (filePaths && filePaths.length > 0) {
|
||||||
const path = filePaths[0];
|
const path = filePaths[0];
|
||||||
|
|
||||||
setForm({ ...form, imageProfileUrl: path });
|
setForm({ ...form, profileImageUrl: path });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export const UserEditProfile = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const avatarUrl = useMemo(() => {
|
const avatarUrl = useMemo(() => {
|
||||||
if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`;
|
if (form.profileImageUrl) return `local:${form.profileImageUrl}`;
|
||||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||||
return null;
|
return null;
|
||||||
}, [form, userProfile]);
|
}, [form, userProfile]);
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./profile.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface UserSignOutModalProps {
|
export interface UserSignOutModalProps {
|
@ -1,27 +1,106 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
export const container = style({
|
export const container = style({
|
||||||
padding: "24px",
|
padding: "24px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
alignItems: "flex-start",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
|
||||||
padding: `${SPACING_UNIT * 3}px`,
|
padding: `${SPACING_UNIT * 3}px`,
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px ${vars.color.border}`,
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||||
borderRadius: "8px",
|
borderRadius: "4px",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
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",
|
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 * as styles from "./settings.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||||
@ -15,12 +13,10 @@ import {
|
|||||||
export function Settings() {
|
export function Settings() {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
const categories = [
|
const categories = {
|
||||||
t("general"),
|
[t("account")]: [t("my_profile"), t("friends")],
|
||||||
t("behavior"),
|
Hydra: [t("general"), t("behavior"), t("download_sources"), "Real-Debrid"],
|
||||||
t("download_sources"),
|
};
|
||||||
"Real-Debrid",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContextProvider>
|
<SettingsContextProvider>
|
||||||
@ -44,21 +40,34 @@ export function Settings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
<div className={styles.content}>
|
<aside className={styles.sidebar}>
|
||||||
<section className={styles.settingsCategories}>
|
{Object.entries(categories).map(([category, items]) => (
|
||||||
{categories.map((category, index) => (
|
<div key={category} className={styles.menuGroup}>
|
||||||
<Button
|
<span className={styles.categoryTitle}>{category}</span>
|
||||||
key={category}
|
|
||||||
theme={
|
|
||||||
currentCategoryIndex === index ? "primary" : "outline"
|
|
||||||
}
|
|
||||||
onClick={() => setCurrentCategoryIndex(index)}
|
|
||||||
>
|
|
||||||
{category}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
<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)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
<h2>{categories[currentCategoryIndex]}</h2>
|
<h2>{categories[currentCategoryIndex]}</h2>
|
||||||
{renderCategory()}
|
{renderCategory()}
|
||||||
</div>
|
</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);
|
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 {
|
export interface UserGame {
|
||||||
objectID: string;
|
objectId: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
title: string;
|
title: string;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
@ -219,7 +219,7 @@ export interface RealDebridUnrestrictLink {
|
|||||||
|
|
||||||
export interface RealDebridAddMagnet {
|
export interface RealDebridAddMagnet {
|
||||||
id: string;
|
id: string;
|
||||||
// URL of the created ressource
|
// URL of the created resource
|
||||||
uri: string;
|
uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,6 +280,8 @@ export interface UserFriend {
|
|||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserFriends {
|
export interface UserFriends {
|
||||||
@ -307,6 +309,10 @@ export interface UserRelation {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserProfileCurrentGame extends GameRunning {
|
||||||
|
sessionDurationInSeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -318,10 +324,10 @@ export interface UserProfile {
|
|||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
totalFriends: number;
|
totalFriends: number;
|
||||||
relation: UserRelation | null;
|
relation: UserRelation | null;
|
||||||
currentGame: GameRunning | null;
|
currentGame: UserProfileCurrentGame | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileProps {
|
export interface UpdateProfileRequest {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
profileImageUrl?: string | null;
|
profileImageUrl?: string | null;
|
||||||
|
98
yarn.lock
98
yarn.lock
@ -579,6 +579,13 @@
|
|||||||
"@types/conventional-commits-parser" "^5.0.0"
|
"@types/conventional-commits-parser" "^5.0.0"
|
||||||
chalk "^5.3.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":
|
"@develar/schema-utils@~2.6.5":
|
||||||
version "2.6.5"
|
version "2.6.5"
|
||||||
resolved "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz"
|
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/sourcemap-codec" "^1.4.10"
|
||||||
"@jridgewell/trace-mapping" "^0.3.24"
|
"@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"
|
version "3.1.2"
|
||||||
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
|
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
|
||||||
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
||||||
@ -1018,6 +1025,14 @@
|
|||||||
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
|
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
|
||||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
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":
|
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
|
||||||
version "0.3.25"
|
version "0.3.25"
|
||||||
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz"
|
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"
|
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
|
||||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
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@*":
|
"@types/accepts@*":
|
||||||
version "1.3.7"
|
version "1.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
|
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"
|
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
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:
|
acorn@^8.11.3, acorn@^8.9.0:
|
||||||
version "8.11.3"
|
version "8.11.3"
|
||||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
|
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"
|
resolved "https://registry.npmjs.org/applescript/-/applescript-1.0.0.tgz"
|
||||||
integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==
|
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:
|
argparse@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
|
||||||
@ -3373,6 +3425,11 @@ create-desktop-shortcuts@^1.11.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
which "2.0.2"
|
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:
|
cross-fetch-ponyfill@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.npmjs.org/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz"
|
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"
|
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
|
||||||
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
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:
|
dir-compare@^3.0.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz"
|
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"
|
bep53-range "^2.0.0"
|
||||||
uint8-util "^2.1.9"
|
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:
|
matcher@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz"
|
||||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
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:
|
tslib@^2.0.3, tslib@^2.5.0, tslib@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
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"
|
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz"
|
||||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
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:
|
verror@^1.10.0:
|
||||||
version "1.10.1"
|
version "1.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb"
|
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"
|
buffer-crc32 "~0.2.3"
|
||||||
fd-slicer "~1.1.0"
|
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:
|
yocto-queue@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user