mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
feat: enabling gif upload
This commit is contained in:
parent
05653500b6
commit
05625e7594
@ -1,14 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import { Ludusavi } from "@main/services";
|
||||
|
||||
const checkGameCloudSyncSupport = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => {
|
||||
const games = await Ludusavi.findGames(shop, objectId);
|
||||
return games.length === 1;
|
||||
};
|
||||
|
||||
registerEvent("checkGameCloudSyncSupport", checkGameCloudSyncSupport);
|
@ -61,7 +61,6 @@ import "./cloud-save/download-game-artifact";
|
||||
import "./cloud-save/get-game-artifacts";
|
||||
import "./cloud-save/get-game-backup-preview";
|
||||
import "./cloud-save/upload-save-game";
|
||||
import "./cloud-save/check-game-cloud-sync-support";
|
||||
import "./cloud-save/delete-game-artifact";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
|
@ -1,56 +1,75 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi, PythonInstance } from "@main/services";
|
||||
import { HydraApi } from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { UpdateProfileRequest, UserProfile } from "@types";
|
||||
import { omit } from "lodash-es";
|
||||
import axios from "axios";
|
||||
|
||||
interface PresignedResponse {
|
||||
presignedUrl: string;
|
||||
profileImageUrl: string;
|
||||
}
|
||||
import { fileTypeFromFile } from "file-type";
|
||||
|
||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
||||
};
|
||||
|
||||
const getNewProfileImageUrl = async (localImageUrl: string) => {
|
||||
const { imagePath, mimeType } =
|
||||
await PythonInstance.processProfileImage(localImageUrl);
|
||||
|
||||
const stats = fs.statSync(imagePath);
|
||||
const uploadImage = async (
|
||||
type: "profile-image" | "background-image",
|
||||
imagePath: string
|
||||
) => {
|
||||
const stat = fs.statSync(imagePath);
|
||||
const fileBuffer = fs.readFileSync(imagePath);
|
||||
const fileSizeInBytes = stats.size;
|
||||
const fileSizeInBytes = stat.size;
|
||||
|
||||
const { presignedUrl, profileImageUrl } =
|
||||
await HydraApi.post<PresignedResponse>(`/presigned-urls/profile-image`, {
|
||||
const response = await HydraApi.post<{ presignedUrl: string }>(
|
||||
`/presigned-urls/${type}`,
|
||||
{
|
||||
imageExt: path.extname(imagePath).slice(1),
|
||||
imageLength: fileSizeInBytes,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
await axios.put(presignedUrl, fileBuffer, {
|
||||
const mimeType = await fileTypeFromFile(imagePath);
|
||||
|
||||
await axios.put(response.presignedUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
"Content-Type": mimeType?.mime,
|
||||
},
|
||||
});
|
||||
|
||||
return profileImageUrl;
|
||||
if (type === "background-image") {
|
||||
return response["backgroundImageUrl"];
|
||||
}
|
||||
|
||||
return response["profileImageUrl"];
|
||||
};
|
||||
|
||||
const updateProfile = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
updateProfile: UpdateProfileRequest
|
||||
) => {
|
||||
if (!updateProfile.profileImageUrl) {
|
||||
return patchUserProfile(omit(updateProfile, "profileImageUrl"));
|
||||
const payload = omit(updateProfile, [
|
||||
"profileImageUrl",
|
||||
"backgroundImageUrl",
|
||||
]);
|
||||
|
||||
if (updateProfile.profileImageUrl) {
|
||||
const profileImageUrl = await uploadImage(
|
||||
"profile-image",
|
||||
updateProfile.profileImageUrl
|
||||
).catch(() => undefined);
|
||||
|
||||
payload["profileImageUrl"] = profileImageUrl;
|
||||
}
|
||||
|
||||
const profileImageUrl = await getNewProfileImageUrl(
|
||||
updateProfile.profileImageUrl
|
||||
).catch(() => undefined);
|
||||
if (updateProfile.backgroundImageUrl) {
|
||||
const backgroundImageUrl = await uploadImage(
|
||||
"background-image",
|
||||
updateProfile.backgroundImageUrl
|
||||
).catch(() => undefined);
|
||||
|
||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||
payload["backgroundImageUrl"] = backgroundImageUrl;
|
||||
}
|
||||
|
||||
return patchUserProfile(payload);
|
||||
};
|
||||
|
||||
registerEvent("updateProfile", updateProfile);
|
||||
|
@ -160,8 +160,6 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getGameArtifacts", objectId, shop),
|
||||
getGameBackupPreview: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
||||
checkGameCloudSyncSupport: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("checkGameCloudSyncSupport", objectId, shop),
|
||||
deleteGameArtifact: (gameArtifactId: string) =>
|
||||
ipcRenderer.invoke("deleteGameArtifact", gameArtifactId),
|
||||
onUploadComplete: (objectId: string, shop: GameShop, cb: () => void) => {
|
||||
|
23
src/renderer/src/components/avatar/avatar.css.ts
Normal file
23
src/renderer/src/components/avatar/avatar.css.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { vars } from "../../theme.css";
|
||||
|
||||
export const profileAvatar = style({
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
cursor: "pointer",
|
||||
color: vars.color.muted,
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const profileAvatarImage = style({
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
});
|
32
src/renderer/src/components/avatar/avatar.tsx
Normal file
32
src/renderer/src/components/avatar/avatar.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { PersonIcon } from "@primer/octicons-react";
|
||||
|
||||
import * as styles from "./avatar.css";
|
||||
|
||||
export interface AvatarProps
|
||||
extends Omit<
|
||||
React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
>,
|
||||
"src"
|
||||
> {
|
||||
size: number;
|
||||
src?: string | null;
|
||||
}
|
||||
|
||||
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
|
||||
return (
|
||||
<div className={styles.profileAvatar} style={{ width: size, height: size }}>
|
||||
{src ? (
|
||||
<img
|
||||
className={styles.profileAvatarImage}
|
||||
alt={alt}
|
||||
src={src}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={size * 0.7} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -60,7 +60,12 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
onMouseEnter={handleHover}
|
||||
>
|
||||
<div className={styles.backdrop}>
|
||||
<img src={game.cover} alt={game.title} className={styles.cover} />
|
||||
<img
|
||||
src={game.cover}
|
||||
alt={game.title}
|
||||
className={styles.cover}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
|
@ -49,7 +49,12 @@ export function Hero() {
|
||||
|
||||
<div className={styles.content}>
|
||||
{game.logo && (
|
||||
<img src={game.logo} width="250px" alt={game.description} />
|
||||
<img
|
||||
src={game.logo}
|
||||
width="250px"
|
||||
alt={game.description}
|
||||
loading="eager"
|
||||
/>
|
||||
)}
|
||||
<p className={styles.description}>{game.description}</p>
|
||||
</div>
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./avatar/avatar";
|
||||
export * from "./bottom-panel/bottom-panel";
|
||||
export * from "./button/button";
|
||||
export * from "./game-card/game-card";
|
||||
@ -12,3 +13,4 @@ export * from "./select-field/select-field";
|
||||
export * from "./toast/toast";
|
||||
export * from "./badge/badge";
|
||||
export * from "./confirmation-modal/confirmation-modal";
|
||||
export * from "./suspense-wrapper/suspense-wrapper";
|
||||
|
@ -31,19 +31,6 @@ export const profileButtonContent = style({
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const profileButtonInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import { PeopleIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { Avatar } from "../avatar/avatar";
|
||||
|
||||
const LONG_POLLING_INTERVAL = 60_000;
|
||||
|
||||
@ -94,17 +95,11 @@ export function SidebarProfile() {
|
||||
onClick={handleProfileClick}
|
||||
>
|
||||
<div className={styles.profileButtonContent}>
|
||||
<div className={styles.profileAvatar}>
|
||||
{userDetails?.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
src={userDetails.profileImageUrl}
|
||||
alt={userDetails.displayName}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
<Avatar
|
||||
size={35}
|
||||
src={userDetails?.profileImageUrl}
|
||||
alt={userDetails?.displayName}
|
||||
/>
|
||||
|
||||
<div className={styles.profileButtonInformation}>
|
||||
<p className={styles.profileButtonTitle}>
|
||||
|
@ -225,6 +225,7 @@ export function Sidebar() {
|
||||
className={styles.gameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.gameIcon} />
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
export interface SuspenseWrapperProps {
|
||||
Component: React.LazyExoticComponent<() => JSX.Element>;
|
||||
}
|
||||
|
||||
export function SuspenseWrapper({ Component }: SuspenseWrapperProps) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
@ -23,13 +23,13 @@ export interface CloudSyncContext {
|
||||
artifacts: GameArtifact[];
|
||||
showCloudSyncModal: boolean;
|
||||
showCloudSyncFilesModal: boolean;
|
||||
supportsCloudSync: boolean | null;
|
||||
backupState: CloudSyncState;
|
||||
setShowCloudSyncModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
downloadGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||
uploadSaveGame: () => Promise<void>;
|
||||
deleteGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
getGameBackupPreview: () => Promise<void>;
|
||||
restoringBackup: boolean;
|
||||
uploadingBackup: boolean;
|
||||
}
|
||||
@ -37,7 +37,6 @@ export interface CloudSyncContext {
|
||||
export const cloudSyncContext = createContext<CloudSyncContext>({
|
||||
backupPreview: null,
|
||||
showCloudSyncModal: false,
|
||||
supportsCloudSync: null,
|
||||
backupState: CloudSyncState.Unknown,
|
||||
setShowCloudSyncModal: () => {},
|
||||
downloadGameArtifact: async () => {},
|
||||
@ -46,6 +45,7 @@ export const cloudSyncContext = createContext<CloudSyncContext>({
|
||||
deleteGameArtifact: async () => {},
|
||||
showCloudSyncFilesModal: false,
|
||||
setShowCloudSyncFilesModal: () => {},
|
||||
getGameBackupPreview: async () => {},
|
||||
restoringBackup: false,
|
||||
uploadingBackup: false,
|
||||
});
|
||||
@ -66,9 +66,6 @@ export function CloudSyncContextProvider({
|
||||
}: CloudSyncContextProviderProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [supportsCloudSync, setSupportsCloudSync] = useState<boolean | null>(
|
||||
null
|
||||
);
|
||||
const [artifacts, setArtifacts] = useState<GameArtifact[]>([]);
|
||||
const [showCloudSyncModal, setShowCloudSyncModal] = useState(false);
|
||||
const [backupPreview, setBackupPreview] = useState<LudusaviBackup | null>(
|
||||
@ -89,21 +86,26 @@ export function CloudSyncContextProvider({
|
||||
);
|
||||
|
||||
const getGameBackupPreview = useCallback(async () => {
|
||||
window.electron.getGameArtifacts(objectId, shop).then((results) => {
|
||||
setArtifacts(results);
|
||||
});
|
||||
|
||||
window.electron
|
||||
.getGameBackupPreview(objectId, shop)
|
||||
.then((preview) => {
|
||||
logger.info("Game backup preview", objectId, shop, preview);
|
||||
if (preview && Object.keys(preview.games).length) {
|
||||
setBackupPreview(preview);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to get game backup preview", objectId, shop, err);
|
||||
});
|
||||
await Promise.allSettled([
|
||||
window.electron.getGameArtifacts(objectId, shop).then((results) => {
|
||||
setArtifacts(results);
|
||||
}),
|
||||
window.electron
|
||||
.getGameBackupPreview(objectId, shop)
|
||||
.then((preview) => {
|
||||
if (preview && Object.keys(preview.games).length) {
|
||||
setBackupPreview(preview);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(
|
||||
"Failed to get game backup preview",
|
||||
objectId,
|
||||
shop,
|
||||
err
|
||||
);
|
||||
}),
|
||||
]);
|
||||
}, [objectId, shop]);
|
||||
|
||||
const uploadSaveGame = useCallback(async () => {
|
||||
@ -152,33 +154,14 @@ export function CloudSyncContextProvider({
|
||||
[getGameBackupPreview]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron
|
||||
.checkGameCloudSyncSupport(objectId, shop)
|
||||
.then((result) => {
|
||||
logger.info("Cloud sync support", objectId, shop, result);
|
||||
setSupportsCloudSync(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to check cloud sync support", err);
|
||||
});
|
||||
}, [objectId, shop, getGameBackupPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
setBackupPreview(null);
|
||||
setArtifacts([]);
|
||||
setSupportsCloudSync(null);
|
||||
setShowCloudSyncModal(false);
|
||||
setRestoringBackup(false);
|
||||
setUploadingBackup(false);
|
||||
}, [objectId, shop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCloudSyncModal) {
|
||||
getGameBackupPreview();
|
||||
}
|
||||
}, [getGameBackupPreview, showCloudSyncModal]);
|
||||
|
||||
const backupState = useMemo(() => {
|
||||
if (!backupPreview) return CloudSyncState.Unknown;
|
||||
if (backupPreview.overall.changedGames.new) return CloudSyncState.New;
|
||||
@ -192,7 +175,6 @@ export function CloudSyncContextProvider({
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
supportsCloudSync,
|
||||
backupPreview,
|
||||
showCloudSyncModal,
|
||||
artifacts,
|
||||
@ -205,6 +187,7 @@ export function CloudSyncContextProvider({
|
||||
downloadGameArtifact,
|
||||
deleteGameArtifact,
|
||||
setShowCloudSyncFilesModal,
|
||||
getGameBackupPreview,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -13,8 +13,9 @@ export interface UserProfileContext {
|
||||
/* Indicates if the current user is viewing their own profile */
|
||||
isMe: boolean;
|
||||
userStats: UserStats | null;
|
||||
|
||||
getUserProfile: () => Promise<void>;
|
||||
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
||||
@ -25,6 +26,8 @@ export const userProfileContext = createContext<UserProfileContext>({
|
||||
isMe: false,
|
||||
userStats: null,
|
||||
getUserProfile: async () => {},
|
||||
setSelectedBackgroundImage: () => {},
|
||||
backgroundImage: "",
|
||||
});
|
||||
|
||||
const { Provider } = userProfileContext;
|
||||
@ -47,6 +50,9 @@ export function UserProfileContextProvider({
|
||||
const [heroBackground, setHeroBackground] = useState(
|
||||
DEFAULT_USER_PROFILE_BACKGROUND
|
||||
);
|
||||
const [selectedBackgroundImage, setSelectedBackgroundImage] = useState("");
|
||||
|
||||
const isMe = userDetails?.id === userProfile?.id;
|
||||
|
||||
const getHeroBackgroundFromImageUrl = async (imageUrl: string) => {
|
||||
const output = await average(imageUrl, {
|
||||
@ -57,6 +63,14 @@ export function UserProfileContextProvider({
|
||||
return `linear-gradient(135deg, ${darkenColor(output as string, 0.5)}, ${darkenColor(output as string, 0.6, 0.5)})`;
|
||||
};
|
||||
|
||||
const getBackgroundImageUrl = () => {
|
||||
if (selectedBackgroundImage && isMe)
|
||||
return `local:${selectedBackgroundImage}`;
|
||||
if (userProfile?.backgroundImageUrl) return userProfile.backgroundImageUrl;
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const { showErrorToast } = useToast();
|
||||
@ -99,8 +113,10 @@ export function UserProfileContextProvider({
|
||||
value={{
|
||||
userProfile,
|
||||
heroBackground,
|
||||
isMe: userDetails?.id === userProfile?.id,
|
||||
isMe,
|
||||
getUserProfile,
|
||||
setSelectedBackgroundImage,
|
||||
backgroundImage: getBackgroundImageUrl(),
|
||||
userStats,
|
||||
}}
|
||||
>
|
||||
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@ -138,10 +138,6 @@ declare global {
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => Promise<LudusaviBackup | null>;
|
||||
checkGameCloudSyncSupport: (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => Promise<boolean>;
|
||||
deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>;
|
||||
onBackupDownloadComplete: (
|
||||
objectId: string,
|
||||
|
@ -15,15 +15,6 @@ import "@fontsource/noto-sans/700.css";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
|
||||
import { App } from "./app";
|
||||
import {
|
||||
Home,
|
||||
Downloads,
|
||||
GameDetails,
|
||||
SearchResults,
|
||||
Settings,
|
||||
Catalogue,
|
||||
Profile,
|
||||
} from "@renderer/pages";
|
||||
|
||||
import { store } from "./store";
|
||||
|
||||
@ -33,6 +24,17 @@ import { AchievementNotification } from "./pages/achievement/notification/achiev
|
||||
import "./workers";
|
||||
import { RepacksContextProvider } from "./context";
|
||||
import { Achievement } from "./pages/achievement/achievements";
|
||||
import { SuspenseWrapper } from "./components";
|
||||
|
||||
const Home = React.lazy(() => import("./pages/home/home"));
|
||||
const GameDetails = React.lazy(
|
||||
() => import("./pages/game-details/game-details")
|
||||
);
|
||||
const Downloads = React.lazy(() => import("./pages/downloads/downloads"));
|
||||
const SearchResults = React.lazy(() => import("./pages/home/search-results"));
|
||||
const Settings = React.lazy(() => import("./pages/settings/settings"));
|
||||
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
|
||||
const Profile = React.lazy(() => import("./pages/profile/profile"));
|
||||
|
||||
Sentry.init({});
|
||||
|
||||
@ -63,13 +65,31 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route element={<App />}>
|
||||
<Route path="/" Component={Home} />
|
||||
<Route path="/catalogue" Component={Catalogue} />
|
||||
<Route path="/downloads" Component={Downloads} />
|
||||
<Route path="/game/:shop/:objectId" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/profile/:userId" Component={Profile} />
|
||||
<Route path="/" element={<SuspenseWrapper Component={Home} />} />
|
||||
<Route
|
||||
path="/catalogue"
|
||||
element={<SuspenseWrapper Component={Catalogue} />}
|
||||
/>
|
||||
<Route
|
||||
path="/downloads"
|
||||
element={<SuspenseWrapper Component={Downloads} />}
|
||||
/>
|
||||
<Route
|
||||
path="/game/:shop/:objectId"
|
||||
element={<SuspenseWrapper Component={GameDetails} />}
|
||||
/>
|
||||
<Route
|
||||
path="/search"
|
||||
element={<SuspenseWrapper Component={SearchResults} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={<SuspenseWrapper Component={Settings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/profile/:userId"
|
||||
element={<SuspenseWrapper Component={Profile} />}
|
||||
/>
|
||||
<Route path="/achievements" Component={Achievement} />
|
||||
</Route>
|
||||
<Route
|
||||
|
@ -14,7 +14,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
|
||||
export function Catalogue() {
|
||||
export default function Catalogue() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { t } = useTranslation("catalogue");
|
||||
|
@ -11,7 +11,7 @@ import { LibraryGame } from "@types";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||
|
||||
export function Downloads() {
|
||||
export default function Downloads() {
|
||||
const { library, updateLibrary } = useLibrary();
|
||||
|
||||
const { t } = useTranslation("downloads");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { average } from "color.js";
|
||||
import Color from "color";
|
||||
|
||||
@ -13,7 +13,7 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import Lottie from "lottie-react";
|
||||
|
||||
import downloadingAnimation from "@renderer/assets/lottie/cloud.json";
|
||||
import cloudAnimation from "@renderer/assets/lottie/cloud.json";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
|
||||
const HERO_ANIMATION_THRESHOLD = 25;
|
||||
@ -36,9 +36,28 @@ export function GameDetailsContent() {
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
|
||||
const { supportsCloudSync, setShowCloudSyncModal } =
|
||||
const { setShowCloudSyncModal, getGameBackupPreview } =
|
||||
useContext(cloudSyncContext);
|
||||
|
||||
const aboutTheGame = useMemo(() => {
|
||||
const aboutTheGame = shopDetails?.about_the_game;
|
||||
if (aboutTheGame) {
|
||||
const document = new DOMParser().parseFromString(
|
||||
aboutTheGame,
|
||||
"text/html"
|
||||
);
|
||||
|
||||
const $images = Array.from(document.querySelectorAll("img"));
|
||||
$images.forEach(($image) => {
|
||||
$image.loading = "lazy";
|
||||
});
|
||||
|
||||
return document.body.outerHTML;
|
||||
}
|
||||
|
||||
return t("no_shop_details");
|
||||
}, [shopDetails, t]);
|
||||
|
||||
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
||||
|
||||
const handleHeroLoad = async () => {
|
||||
@ -87,6 +106,10 @@ export function GameDetailsContent() {
|
||||
setShowCloudSyncModal(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getGameBackupPreview();
|
||||
}, [getGameBackupPreview]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
||||
<img
|
||||
@ -121,32 +144,30 @@ export function GameDetailsContent() {
|
||||
alt={game?.title}
|
||||
/>
|
||||
|
||||
{supportsCloudSync && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.cloudSyncButton}
|
||||
onClick={handleCloudSaveButtonClick}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.cloudSyncButton}
|
||||
onClick={handleCloudSaveButtonClick}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 16 + 4,
|
||||
height: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 16 + 4,
|
||||
height: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Lottie
|
||||
animationData={downloadingAnimation}
|
||||
loop
|
||||
autoplay
|
||||
style={{ width: 26, position: "absolute", top: -3 }}
|
||||
/>
|
||||
</div>
|
||||
{t("cloud_save")}
|
||||
</button>
|
||||
)}
|
||||
<Lottie
|
||||
animationData={cloudAnimation}
|
||||
loop
|
||||
autoplay
|
||||
style={{ width: 26, position: "absolute", top: -3 }}
|
||||
/>
|
||||
</div>
|
||||
{t("cloud_save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,7 +181,7 @@ export function GameDetailsContent() {
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: shopDetails?.about_the_game ?? t("no_shop_details"),
|
||||
__html: aboutTheGame,
|
||||
}}
|
||||
className={styles.description}
|
||||
/>
|
||||
|
@ -29,7 +29,7 @@ import { Downloader, getDownloadersForUri } from "@shared";
|
||||
import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal";
|
||||
import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal";
|
||||
|
||||
export function GameDetails() {
|
||||
export default function GameDetails() {
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
const [randomizerLocked, setRandomizerLocked] = useState(false);
|
||||
|
||||
|
@ -16,7 +16,7 @@ import Lottie, { type LottieRefCurrentProps } from "lottie-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { CatalogueCategory } from "@shared";
|
||||
|
||||
export function Home() {
|
||||
export default function Home() {
|
||||
const { t } = useTranslation("home");
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
@ -17,7 +17,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
export function SearchResults() {
|
||||
export default function SearchResults() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { t } = useTranslation("home");
|
||||
|
@ -1,7 +0,0 @@
|
||||
export * from "./home/home";
|
||||
export * from "./game-details/game-details";
|
||||
export * from "./downloads/downloads";
|
||||
export * from "./home/search-results";
|
||||
export * from "./settings/settings";
|
||||
export * from "./catalogue/catalogue";
|
||||
export * from "./profile/profile";
|
@ -3,28 +3,18 @@ import { globalStyle, style } from "@vanilla-extract/css";
|
||||
|
||||
export const profileAvatarEditContainer = style({
|
||||
alignSelf: "center",
|
||||
width: "128px",
|
||||
height: "128px",
|
||||
// width: "132px",
|
||||
// height: "132px",
|
||||
display: "flex",
|
||||
borderRadius: "4px",
|
||||
// borderRadius: "4px",
|
||||
color: vars.color.body,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const profileAvatarEditOverlay = style({
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
|
@ -2,8 +2,9 @@ import { useContext, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import { DeviceCameraIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Link,
|
||||
Modal,
|
||||
@ -111,14 +112,14 @@ export function EditProfileModal(
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
|
||||
const { imagePath } = await window.electron
|
||||
.processProfileImage(path)
|
||||
.catch(() => {
|
||||
showErrorToast(t("image_process_failure"));
|
||||
return { imagePath: null };
|
||||
});
|
||||
// const { imagePath } = await window.electron
|
||||
// .processProfileImage(path)
|
||||
// .catch(() => {
|
||||
// showErrorToast(t("image_process_failure"));
|
||||
// return { imagePath: null };
|
||||
// });
|
||||
|
||||
onChange(imagePath);
|
||||
onChange(path);
|
||||
}
|
||||
};
|
||||
|
||||
@ -138,15 +139,11 @@ export function EditProfileModal(
|
||||
className={styles.profileAvatarEditContainer}
|
||||
onClick={handleChangeProfileAvatar}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userDetails?.displayName}
|
||||
src={imageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={96} />
|
||||
)}
|
||||
<Avatar
|
||||
size={128}
|
||||
src={imageUrl}
|
||||
alt={userDetails?.displayName}
|
||||
/>
|
||||
|
||||
<div className={styles.profileAvatarEditOverlay}>
|
||||
<DeviceCameraIcon size={38} />
|
||||
|
@ -4,8 +4,7 @@ import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./profile-content.css";
|
||||
import { Link } from "@renderer/components";
|
||||
import { PersonIcon } from "@primer/octicons-react";
|
||||
import { Avatar, Link } from "@renderer/components";
|
||||
|
||||
export function FriendsBox() {
|
||||
const { userProfile, userStats } = useContext(userProfileContext);
|
||||
@ -30,17 +29,11 @@ export function FriendsBox() {
|
||||
{userProfile?.friends.map((friend) => (
|
||||
<li key={friend.id}>
|
||||
<Link to={`/profile/${friend.id}`} className={styles.listItem}>
|
||||
{friend.profileImageUrl ? (
|
||||
<img
|
||||
src={friend.profileImageUrl!}
|
||||
alt={friend.displayName}
|
||||
className={styles.listItemImage}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.defaultAvatarWrapper}>
|
||||
<PersonIcon size={16} />
|
||||
</div>
|
||||
)}
|
||||
<Avatar
|
||||
size={32}
|
||||
src={friend.profileImageUrl}
|
||||
alt={friend.displayName}
|
||||
/>
|
||||
|
||||
<span className={styles.friendName}>{friend.displayName}</span>
|
||||
</Link>
|
||||
|
@ -1,17 +1,5 @@
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
const animateBackground = keyframes({
|
||||
"0%": {
|
||||
backgroundPosition: "0% 50%",
|
||||
},
|
||||
"50%": {
|
||||
backgroundPosition: "100% 50%",
|
||||
},
|
||||
"100%": {
|
||||
backgroundPosition: "0% 50%",
|
||||
},
|
||||
});
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const profileContentBox = style({
|
||||
display: "flex",
|
||||
@ -74,7 +62,7 @@ export const heroPanel = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "space-between",
|
||||
backdropFilter: `blur(10px)`,
|
||||
backdropFilter: `blur(15px)`,
|
||||
borderTop: `solid 1px rgba(255, 255, 255, 0.1)`,
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.3)",
|
||||
@ -99,25 +87,3 @@ export const currentGameDetails = style({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const xdTotal = style({
|
||||
background: `linear-gradient(
|
||||
60deg,
|
||||
#f79533,
|
||||
#f37055,
|
||||
#ef4e7b,
|
||||
#a166ab,
|
||||
#5073b8,
|
||||
#1098ad,
|
||||
#07b39b,
|
||||
#6fba82
|
||||
)`,
|
||||
width: "102px",
|
||||
minWidth: "102px",
|
||||
height: "102px",
|
||||
animation: `${animateBackground} 4s ease alternate infinite`,
|
||||
backgroundSize: "300% 300%",
|
||||
zIndex: -1,
|
||||
borderRadius: "4px",
|
||||
position: "absolute",
|
||||
});
|
||||
|
@ -8,13 +8,11 @@ import {
|
||||
CheckCircleFillIcon,
|
||||
PencilIcon,
|
||||
PersonAddIcon,
|
||||
PersonIcon,
|
||||
SignOutIcon,
|
||||
UploadIcon,
|
||||
XCircleFillIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
import { Avatar, Button, Link } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useAppSelector,
|
||||
@ -28,16 +26,21 @@ import { useNavigate } from "react-router-dom";
|
||||
import type { FriendRequestAction } from "@types";
|
||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
||||
|
||||
type FriendAction =
|
||||
| FriendRequestAction
|
||||
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
|
||||
|
||||
const backgroundImageLayer =
|
||||
"linear-gradient(135deg, rgb(0 0 0 / 50%), rgb(0 0 0 / 60%))";
|
||||
|
||||
export function ProfileHero() {
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||
|
||||
const { isMe, getUserProfile, userProfile } = useContext(userProfileContext);
|
||||
const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } =
|
||||
useContext(userProfileContext);
|
||||
const {
|
||||
signOut,
|
||||
updateFriendRequestState,
|
||||
@ -48,8 +51,6 @@ export function ProfileHero() {
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const [hero, setHero] = useState("");
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { formatDistance } = useDate();
|
||||
|
||||
@ -186,6 +187,7 @@ export function ProfileHero() {
|
||||
handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")
|
||||
}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
>
|
||||
<XCircleFillIcon />
|
||||
{t("undo_friendship")}
|
||||
@ -260,35 +262,6 @@ export function ProfileHero() {
|
||||
return userProfile?.currentGame;
|
||||
}, [isMe, userProfile, gameRunning]);
|
||||
|
||||
const handleChangeCoverClick = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
|
||||
setHero(path);
|
||||
|
||||
// onChange(imagePath);
|
||||
}
|
||||
};
|
||||
|
||||
const getImageUrl = () => {
|
||||
if (hero) return `local:${hero}`;
|
||||
// if (userDetails?.profileImageUrl) return userDetails.profileImageUrl;
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
// const imageUrl = getImageUrl();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <ConfirmationModal
|
||||
@ -304,21 +277,26 @@ export function ProfileHero() {
|
||||
onClose={() => setShowEditProfileModal(false)}
|
||||
/>
|
||||
|
||||
<section className={styles.profileContentBox}>
|
||||
<img
|
||||
src={getImageUrl()}
|
||||
alt=""
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{ background: heroBackground }}
|
||||
>
|
||||
{backgroundImage && (
|
||||
<img
|
||||
src={backgroundImage}
|
||||
alt=""
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, rgb(0 0 0 / 70%), rgb(0 0 0 / 60%))",
|
||||
background: backgroundImage ? backgroundImageLayer : "transparent",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: 1,
|
||||
@ -330,16 +308,11 @@ export function ProfileHero() {
|
||||
className={styles.profileAvatarButton}
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
<div className={styles.xdTotal} />
|
||||
{userProfile?.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile?.displayName}
|
||||
src={userProfile?.profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
)}
|
||||
<Avatar
|
||||
size={96}
|
||||
alt={userProfile?.displayName}
|
||||
src={userProfile?.profileImageUrl}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
@ -379,28 +352,14 @@ export function ProfileHero() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
theme="outline"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
borderColor: vars.color.body,
|
||||
}}
|
||||
onClick={handleChangeCoverClick}
|
||||
>
|
||||
<UploadIcon />
|
||||
Upload cover
|
||||
</Button>
|
||||
<UploadBackgroundImageButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.heroPanel}
|
||||
// style={{ background: heroBackground }}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, rgb(0 0 0 / 70%), rgb(0 0 0 / 60%))",
|
||||
background: backgroundImage ? backgroundImageLayer : heroBackground,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
@ -6,7 +6,7 @@ import * as styles from "./profile.css";
|
||||
import { UserProfileContextProvider } from "@renderer/context";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export function Profile() {
|
||||
export default function Profile() {
|
||||
const { userId } = useParams();
|
||||
|
||||
return (
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { vars } from "../../../theme.css";
|
||||
|
||||
export const uploadBackgroundImageButton = style({
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
borderColor: vars.color.body,
|
||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.8)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
backdropFilter: "blur(20px)",
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
import { UploadIcon } from "@primer/octicons-react";
|
||||
import { Button } from "@renderer/components";
|
||||
import { useContext, useState } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
|
||||
import * as styles from "./upload-background-image-button.css";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
|
||||
export function UploadBackgroundImageButton() {
|
||||
const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] =
|
||||
useState(false);
|
||||
|
||||
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
|
||||
const { patchUser } = useUserDetails();
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const handleChangeCoverClick = async () => {
|
||||
try {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
|
||||
setSelectedBackgroundImage(path);
|
||||
setIsUploadingBackgorundImage(true);
|
||||
|
||||
await patchUser({ backgroundImageUrl: path });
|
||||
|
||||
showSuccessToast("Background image updated");
|
||||
}
|
||||
} finally {
|
||||
setIsUploadingBackgorundImage(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isMe) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.uploadBackgroundImageButton}
|
||||
onClick={handleChangeCoverClick}
|
||||
disabled={isUploadingBackgroundImage}
|
||||
>
|
||||
<UploadIcon />
|
||||
{isUploadingBackgroundImage ? "Uploading..." : "Upload background"}
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -15,7 +15,7 @@ import { SettingsPrivacy } from "./settings-privacy";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function Settings() {
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
|
@ -1,11 +1,8 @@
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PersonIcon,
|
||||
XCircleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { CheckCircleIcon, XCircleIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./user-friend-modal.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar } from "@renderer/components";
|
||||
|
||||
export type UserFriendItemProps = {
|
||||
userId: string;
|
||||
@ -109,17 +106,8 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||
return (
|
||||
<div className={styles.friendListContainer}>
|
||||
<div className={styles.friendListButton} style={{ cursor: "inherit" }}>
|
||||
<div className={styles.friendAvatarContainer}>
|
||||
{profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={displayName}
|
||||
src={profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@ -154,17 +142,7 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||
className={styles.friendListButton}
|
||||
onClick={() => props.onClickItem(userId)}
|
||||
>
|
||||
<div className={styles.friendAvatarContainer}>
|
||||
{profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={displayName}
|
||||
src={profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
@ -1,20 +1,6 @@
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const friendAvatarContainer = style({
|
||||
width: "35px",
|
||||
minWidth: "35px",
|
||||
height: "35px",
|
||||
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)",
|
||||
});
|
||||
|
||||
export const friendListDisplayName = style({
|
||||
fontWeight: "bold",
|
||||
fontSize: vars.size.body,
|
||||
@ -24,12 +10,6 @@ export const friendListDisplayName = style({
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const friendListContainer = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
|
@ -205,6 +205,7 @@ export interface UserDetails {
|
||||
username: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
backgroundImageUrl: string | null;
|
||||
profileVisibility: ProfileVisibility;
|
||||
bio: string;
|
||||
}
|
||||
@ -213,6 +214,7 @@ export interface UserProfile {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
backgroundImageUrl: string | null;
|
||||
profileVisibility: ProfileVisibility;
|
||||
libraryGames: UserGame[];
|
||||
recentGames: UserGame[];
|
||||
@ -227,6 +229,7 @@ export interface UpdateProfileRequest {
|
||||
displayName?: string;
|
||||
profileVisibility?: ProfileVisibility;
|
||||
profileImageUrl?: string | null;
|
||||
backgroundImageUrl?: string | null;
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user