diff --git a/package.json b/package.json index 31bac590..53554bd2 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "jsdom": "^24.0.0", "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", + "mime": "^4.0.3", "parse-torrent": "^11.0.16", "piscina": "^4.5.1", "ps-list": "^8.1.1", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 096b14a1..190fd56b 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -42,6 +42,7 @@ import "./download-sources/sync-download-sources"; import "./auth/signout"; import "./user/get-user"; import "./profile/get-me"; +import "./profile/update-profile"; ipcMain.handle("ping", () => "pong"); ipcMain.handle("getVersion", () => app.getVersion()); diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts new file mode 100644 index 00000000..70ad1322 --- /dev/null +++ b/src/main/events/profile/update-profile.ts @@ -0,0 +1,59 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services/hydra-api"; +import axios from "axios"; +import fs from "node:fs"; +import mime from "mime"; + +const patchUserProfile = (displayName: string, imageUrl?: string) => { + return; + if (imageUrl) { + return HydraApi.patch("/profile", { + displayName, + imageUrl, + }); + } else { + return HydraApi.patch("/profile", { + displayName, + }); + } +}; + +const updateProfile = async ( + _event: Electron.IpcMainInvokeEvent, + displayName: string, + newProfileImagePath: string | null +): Promise => { + if (!newProfileImagePath) { + patchUserProfile(displayName); + return; + } + + const stats = fs.statSync(newProfileImagePath); + const fileBuffer = fs.readFileSync(newProfileImagePath); + const fileSizeInBytes = stats.size; + + const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, { + imageExt: newProfileImagePath.split(".").at(-1), + imageLength: fileSizeInBytes, + }) + .then(async (preSignedResponse) => { + const { presignedUrl, profileImageUrl } = preSignedResponse.data; + + const mimeType = mime.getType(newProfileImagePath); + + await axios.put(presignedUrl, fileBuffer, { + headers: { + "Content-Type": mimeType, + }, + }); + return profileImageUrl; + }) + .catch(() => { + return undefined; + }); + + console.log(profileImageUrl); + patchUserProfile(displayName, profileImageUrl); +}; + +registerEvent("updateProfile", updateProfile); diff --git a/src/preload/index.ts b/src/preload/index.ts index faae6ef9..607c0fb0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -128,6 +128,8 @@ contextBridge.exposeInMainWorld("electron", { /* Profile */ getMe: () => ipcRenderer.invoke("getMe"), + updateProfile: (displayName: string, newProfileImagePath: string | null) => + ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath), /* User */ getUser: (userId: string) => ipcRenderer.invoke("getUser", userId), diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 48c7bdc6..a785ad1c 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -121,6 +121,10 @@ declare global { /* Profile */ getMe: () => Promise; + updateProfile: ( + displayName: string, + newProfileImagePath: string | null + ) => Promise; } interface Window { diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index 600f9128..739afda7 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -3,7 +3,7 @@ import cn from "classnames"; import * as styles from "./user.css"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import { useDate, useUserDetails } from "@renderer/hooks"; @@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom"; import { buildGameDetailsPath } from "@renderer/helpers"; import { PersonIcon } from "@primer/octicons-react"; import { Button } from "@renderer/components"; +import { UserEditProfileModal } from "./user-edit-modal"; const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; @@ -23,6 +24,8 @@ export function UserContent({ userProfile }: ProfileContentProps) { const { userDetails, profileBackground, signOut } = useUserDetails(); + const [showEditProfileModal, setShowEditProfileModal] = useState(false); + const navigate = useNavigate(); const numberFormatter = useMemo(() => { @@ -54,6 +57,10 @@ export function UserContent({ userProfile }: ProfileContentProps) { navigate(buildGameDetailsPath(game)); }; + const handleEditProfile = () => { + setShowEditProfileModal(true); + }; + const handleSignout = async () => { await signOut(); navigate("/"); @@ -69,10 +76,17 @@ export function UserContent({ userProfile }: ProfileContentProps) { return ( <> + setShowEditProfileModal(false)} + userProfile={userProfile} + /> +
@@ -93,9 +107,23 @@ export function UserContent({ userProfile }: ProfileContentProps) { {isMe && (
- +
+ <> + + + + +
)}
diff --git a/src/renderer/src/pages/user/user-edit-modal.tsx b/src/renderer/src/pages/user/user-edit-modal.tsx new file mode 100644 index 00000000..24b0cfd7 --- /dev/null +++ b/src/renderer/src/pages/user/user-edit-modal.tsx @@ -0,0 +1,90 @@ +import { Button, Modal, TextField } from "@renderer/components"; +import { UserProfile } from "@types"; +import * as styles from "./user.css"; +import { PersonIcon } from "@primer/octicons-react"; +import { SPACING_UNIT } from "@renderer/theme.css"; +import { useState } from "react"; + +export interface UserEditProfileModalProps { + userProfile: UserProfile; + visible: boolean; + onClose: () => void; +} + +export const UserEditProfileModal = ({ + userProfile, + visible, + onClose, +}: UserEditProfileModalProps) => { + const [displayName, setDisplayName] = useState(userProfile.displayName); + const [newImagePath, setNewImagePath] = useState(null); + + const handleChangeProfileAvatar = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: "Profile avatar", + extensions: ["jpg", "png", "gif"], + }, + ], + }); + + const path = filePaths[0]; + console.log(path); + + setNewImagePath(path); + }; + + const handleSaveProfile = async () => { + await window.electron + .updateProfile(displayName, newImagePath) + .catch((err) => { + console.log("errro", err); + }); + setNewImagePath(null); + onClose(); + }; + return ( + <> + +
+ + + setDisplayName(e.target.value)} + /> + +
+
+ + ); +}; diff --git a/src/renderer/src/pages/user/user.css.ts b/src/renderer/src/pages/user/user.css.ts index 6ec6b820..ceb74089 100644 --- a/src/renderer/src/pages/user/user.css.ts +++ b/src/renderer/src/pages/user/user.css.ts @@ -12,7 +12,6 @@ export const wrapper = style({ export const profileContentBox = style({ display: "flex", gap: `${SPACING_UNIT * 3}px`, - padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 2}px`, alignItems: "center", borderRadius: "4px", border: `solid 1px ${vars.color.border}`, @@ -36,12 +35,35 @@ export const profileAvatarContainer = style({ boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", }); +export const profileAvatarEditContainer = style({ + width: "128px", + height: "128px", + borderRadius: "50%", + display: "flex", + justifyContent: "center", + alignItems: "center", + backgroundColor: vars.color.background, + position: "relative", + overflow: "hidden", + border: `solid 1px ${vars.color.border}`, + boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", +}); + export const profileAvatar = style({ - width: "96px", - height: "96px", + height: "100%", objectFit: "cover", }); +export const profileAvatarEditOverlay = style({ + position: "absolute", + width: "100%", + height: "100%", + backgroundColor: "#00000055", + color: vars.color.muted, + zIndex: 1, + cursor: "pointer", +}); + export const profileInformation = style({ display: "flex", flexDirection: "column", diff --git a/yarn.lock b/yarn.lock index f438ecf4..be926a90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4518,6 +4518,11 @@ mime@^2.5.2: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +mime@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/mime/-/mime-4.0.3.tgz#cd4a1aa052fc980dfc34f111fe1be9e8b878c5d2" + integrity sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ== + mimic-fn@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"