mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding modal to edit profile
This commit is contained in:
parent
79ca354da1
commit
af69509c61
9 changed files with 219 additions and 7 deletions
|
@ -61,6 +61,7 @@
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
|
"mime": "^4.0.3",
|
||||||
"parse-torrent": "^11.0.16",
|
"parse-torrent": "^11.0.16",
|
||||||
"piscina": "^4.5.1",
|
"piscina": "^4.5.1",
|
||||||
"ps-list": "^8.1.1",
|
"ps-list": "^8.1.1",
|
||||||
|
|
|
@ -42,6 +42,7 @@ import "./download-sources/sync-download-sources";
|
||||||
import "./auth/signout";
|
import "./auth/signout";
|
||||||
import "./user/get-user";
|
import "./user/get-user";
|
||||||
import "./profile/get-me";
|
import "./profile/get-me";
|
||||||
|
import "./profile/update-profile";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
|
|
59
src/main/events/profile/update-profile.ts
Normal file
59
src/main/events/profile/update-profile.ts
Normal file
|
@ -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<any> => {
|
||||||
|
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);
|
|
@ -128,6 +128,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
|
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
||||||
|
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||||
|
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
|
@ -121,6 +121,10 @@ declare global {
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
|
updateProfile: (
|
||||||
|
displayName: string,
|
||||||
|
newProfileImagePath: string | null
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import cn from "classnames";
|
||||||
|
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { useDate, useUserDetails } from "@renderer/hooks";
|
import { useDate, useUserDetails } from "@renderer/hooks";
|
||||||
|
@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonIcon } from "@primer/octicons-react";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
|
import { UserEditProfileModal } from "./user-edit-modal";
|
||||||
|
|
||||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
||||||
|
|
||||||
const { userDetails, profileBackground, signOut } = useUserDetails();
|
const { userDetails, profileBackground, signOut } = useUserDetails();
|
||||||
|
|
||||||
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const numberFormatter = useMemo(() => {
|
const numberFormatter = useMemo(() => {
|
||||||
|
@ -54,6 +57,10 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
||||||
navigate(buildGameDetailsPath(game));
|
navigate(buildGameDetailsPath(game));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditProfile = () => {
|
||||||
|
setShowEditProfileModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSignout = async () => {
|
const handleSignout = async () => {
|
||||||
await signOut();
|
await signOut();
|
||||||
navigate("/");
|
navigate("/");
|
||||||
|
@ -69,10 +76,17 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<UserEditProfileModal
|
||||||
|
visible={showEditProfileModal}
|
||||||
|
onClose={() => setShowEditProfileModal(false)}
|
||||||
|
userProfile={userProfile}
|
||||||
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className={styles.profileContentBox}
|
className={styles.profileContentBox}
|
||||||
style={{
|
style={{
|
||||||
background: profileContentBoxBackground,
|
background: profileContentBoxBackground,
|
||||||
|
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.profileAvatarContainer}>
|
<div className={styles.profileAvatarContainer}>
|
||||||
|
@ -93,9 +107,23 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
||||||
|
|
||||||
{isMe && (
|
{isMe && (
|
||||||
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
|
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Button theme="outline" onClick={handleEditProfile}>
|
||||||
|
Editar perfil
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button theme="danger" onClick={handleSignout}>
|
<Button theme="danger" onClick={handleSignout}>
|
||||||
{t("sign_out")}
|
{t("sign_out")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
90
src/renderer/src/pages/user/user-edit-modal.tsx
Normal file
90
src/renderer/src/pages/user/user-edit-modal.tsx
Normal file
|
@ -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<string | null>(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 (
|
||||||
|
<>
|
||||||
|
<Modal visible={visible} title="Editar Perfil" onClose={onClose}>
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
width: "300px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={styles.profileAvatarEditContainer}
|
||||||
|
onClick={handleChangeProfileAvatar}
|
||||||
|
>
|
||||||
|
{userProfile.profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={userProfile.displayName}
|
||||||
|
src={newImagePath ?? userProfile.profileImageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={72} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Nome de exibição"
|
||||||
|
value={displayName}
|
||||||
|
containerProps={{ style: { width: "100%" } }}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button style={{ alignSelf: "end" }} onClick={handleSaveProfile}>
|
||||||
|
Salvar{" "}
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -12,7 +12,6 @@ export const wrapper = style({
|
||||||
export const profileContentBox = style({
|
export const profileContentBox = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
border: `solid 1px ${vars.color.border}`,
|
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)",
|
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({
|
export const profileAvatar = style({
|
||||||
width: "96px",
|
height: "100%",
|
||||||
height: "96px",
|
|
||||||
objectFit: "cover",
|
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({
|
export const profileInformation = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
|
@ -4518,6 +4518,11 @@ mime@^2.5.2:
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
|
||||||
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
|
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:
|
mimic-fn@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue