mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: process profile image
This commit is contained in:
parent
a295003ad4
commit
797a09f583
12 changed files with 207 additions and 104 deletions
|
@ -53,6 +53,7 @@ import "./profile/get-me";
|
|||
import "./profile/undo-friendship";
|
||||
import "./profile/update-friend-request";
|
||||
import "./profile/update-profile";
|
||||
import "./profile/process-profile-image";
|
||||
import "./profile/send-friend-request";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
|
||||
|
|
11
src/main/events/profile/process-profile-image.ts
Normal file
11
src/main/events/profile/process-profile-image.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { PythonInstance } from "@main/services";
|
||||
|
||||
const processProfileImage = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
path: string
|
||||
) => {
|
||||
return await PythonInstance.processProfileImage(path);
|
||||
};
|
||||
|
||||
registerEvent("processProfileImage", processProfileImage);
|
|
@ -1,46 +1,54 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import axios from "axios";
|
||||
import { HydraApi, PythonInstance } from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileTypeFromFile } from "file-type";
|
||||
import { UpdateProfileProps, UserProfile } from "@types";
|
||||
import type { UpdateProfileProps, UserProfile } from "@types";
|
||||
import { omit } from "lodash-es";
|
||||
import axios from "axios";
|
||||
|
||||
interface PresignedResponse {
|
||||
presignedUrl: string;
|
||||
profileImageUrl: string;
|
||||
}
|
||||
|
||||
const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
|
||||
return HydraApi.patch("/profile", updateProfile);
|
||||
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
||||
};
|
||||
|
||||
const getNewProfileImageUrl = async (localImageUrl: string) => {
|
||||
const { imagePath, mimeType } =
|
||||
await PythonInstance.processProfileImage(localImageUrl);
|
||||
|
||||
const stats = fs.statSync(imagePath);
|
||||
const fileBuffer = fs.readFileSync(imagePath);
|
||||
const fileSizeInBytes = stats.size;
|
||||
|
||||
const { presignedUrl, profileImageUrl } =
|
||||
await HydraApi.post<PresignedResponse>(`/presigned-urls/profile-image`, {
|
||||
imageExt: path.extname(imagePath).slice(1),
|
||||
imageLength: fileSizeInBytes,
|
||||
});
|
||||
|
||||
await axios.put(presignedUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
},
|
||||
});
|
||||
|
||||
return profileImageUrl;
|
||||
};
|
||||
|
||||
const updateProfile = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
updateProfile: UpdateProfileProps
|
||||
): Promise<UserProfile> => {
|
||||
) => {
|
||||
if (!updateProfile.profileImageUrl) {
|
||||
return patchUserProfile(updateProfile);
|
||||
return patchUserProfile(omit(updateProfile, "profileImageUrl"));
|
||||
}
|
||||
|
||||
const newProfileImagePath = updateProfile.profileImageUrl;
|
||||
|
||||
const stats = fs.statSync(newProfileImagePath);
|
||||
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
||||
const fileSizeInBytes = stats.size;
|
||||
|
||||
const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, {
|
||||
imageExt: path.extname(newProfileImagePath).slice(1),
|
||||
imageLength: fileSizeInBytes,
|
||||
})
|
||||
.then(async (preSignedResponse) => {
|
||||
const { presignedUrl, profileImageUrl } = preSignedResponse;
|
||||
|
||||
const mimeType = await fileTypeFromFile(newProfileImagePath);
|
||||
|
||||
await axios.put(presignedUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": mimeType?.mime,
|
||||
},
|
||||
});
|
||||
return profileImageUrl as string;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
const profileImageUrl = await getNewProfileImageUrl(
|
||||
updateProfile.profileImageUrl
|
||||
).catch(() => undefined);
|
||||
|
||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||
};
|
||||
|
|
|
@ -166,6 +166,14 @@ export class PythonInstance {
|
|||
this.downloadingGameId = -1;
|
||||
}
|
||||
|
||||
static async processProfileImage(imagePath: string) {
|
||||
return this.rpc
|
||||
.post<{ imagePath: string; mimeType: string }>("/profile-image", {
|
||||
image_path: imagePath,
|
||||
})
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
private static async handleRpcError(_error: unknown) {
|
||||
await this.rpc.get("/healthcheck").catch(() => {
|
||||
logger.error(
|
||||
|
|
|
@ -141,6 +141,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
ipcRenderer.invoke("undoFriendship", userId),
|
||||
updateProfile: (updateProfile: UpdateProfileProps) =>
|
||||
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||
processProfileImage: (imagePath: string) =>
|
||||
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||
|
|
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
|
@ -144,6 +144,9 @@ declare global {
|
|||
getMe: () => Promise<UserProfile | null>;
|
||||
undoFriendship: (userId: string) => Promise<void>;
|
||||
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||
processProfileImage: (
|
||||
path: string
|
||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||
updateFriendRequest: (
|
||||
userId: string,
|
||||
|
|
|
@ -5,7 +5,8 @@ import { UserProfile } from "@types";
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "../user.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||
|
||||
export interface UserEditProfileProps {
|
||||
userProfile: UserProfile;
|
||||
|
@ -21,9 +22,10 @@ export const UserEditProfile = ({
|
|||
const [form, setForm] = useState({
|
||||
displayName: userProfile.displayName,
|
||||
profileVisibility: userProfile.profileVisibility,
|
||||
imageProfileUrl: null as string | null,
|
||||
profileImageUrl: null as string | null,
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isLoadingImage, setIsLoadingImage] = useState(false);
|
||||
|
||||
const { patchUser } = useUserDetails();
|
||||
|
||||
|
@ -53,9 +55,16 @@ export const UserEditProfile = ({
|
|||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
setIsLoadingImage(true);
|
||||
|
||||
setForm({ ...form, imageProfileUrl: path });
|
||||
const { imagePath } = await window.electron
|
||||
.processProfileImage(filePaths[0])
|
||||
.catch(() => {
|
||||
return { imagePath: null };
|
||||
})
|
||||
.finally(() => setIsLoadingImage(false));
|
||||
|
||||
setForm({ ...form, profileImageUrl: imagePath });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -85,66 +94,78 @@ export const UserEditProfile = ({
|
|||
});
|
||||
};
|
||||
|
||||
const avatarUrl = useMemo(() => {
|
||||
if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`;
|
||||
const profileImageUrl = useMemo(() => {
|
||||
if (form.profileImageUrl) return `local:${form.profileImageUrl}`;
|
||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||
return null;
|
||||
}, [form, userProfile]);
|
||||
|
||||
const profileImageContent = () => {
|
||||
if (isLoadingImage) {
|
||||
return <Skeleton className={styles.profileAvatar} />;
|
||||
}
|
||||
|
||||
if (profileImageUrl) {
|
||||
return (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile.displayName}
|
||||
src={profileImageUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <PersonIcon size={96} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSaveProfile}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
width: "350px",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileAvatarEditContainer}
|
||||
onClick={handleChangeProfileAvatar}
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<form
|
||||
onSubmit={handleSaveProfile}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
width: "350px",
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile.displayName}
|
||||
src={avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={96} />
|
||||
)}
|
||||
<div className={styles.editProfileImageBadge}>
|
||||
<DeviceCameraIcon size={16} />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileAvatarEditContainer}
|
||||
onClick={handleChangeProfileAvatar}
|
||||
>
|
||||
{profileImageContent()}
|
||||
<div className={styles.editProfileImageBadge}>
|
||||
<DeviceCameraIcon size={16} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<TextField
|
||||
label={t("display_name")}
|
||||
value={form.displayName}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={50}
|
||||
containerProps={{ style: { width: "100%" } }}
|
||||
onChange={(e) => setForm({ ...form, displayName: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label={t("display_name")}
|
||||
value={form.displayName}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={50}
|
||||
containerProps={{ style: { width: "100%" } }}
|
||||
onChange={(e) => setForm({ ...form, displayName: e.target.value })}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
label={t("privacy")}
|
||||
value={form.profileVisibility}
|
||||
onChange={handleProfileVisibilityChange}
|
||||
options={profileVisibilityOptions.map((visiblity) => ({
|
||||
key: visiblity.value,
|
||||
value: visiblity.value,
|
||||
label: visiblity.label,
|
||||
}))}
|
||||
/>
|
||||
<SelectField
|
||||
label={t("privacy")}
|
||||
value={form.profileVisibility}
|
||||
onChange={handleProfileVisibilityChange}
|
||||
options={profileVisibilityOptions.map((visiblity) => ({
|
||||
key: visiblity.value,
|
||||
value: visiblity.value,
|
||||
label: visiblity.label,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Button disabled={isSaving} style={{ alignSelf: "end" }} type="submit">
|
||||
{isSaving ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</form>
|
||||
<Button disabled={isSaving} style={{ alignSelf: "end" }} type="submit">
|
||||
{isSaving ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</form>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue