Skip to content

Commit

Permalink
feat: process profile image
Browse files Browse the repository at this point in the history
  • Loading branch information
zamitto committed Sep 13, 2024
1 parent a295003 commit 797a09f
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 107 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
"electron-log": "^5.1.4",
"electron-updater": "^6.1.8",
"fetch-cookie": "^3.0.1",
"file-type": "^19.0.0",
"flexsearch": "^0.7.43",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ cx_Logging; sys_platform == 'win32'
lief; sys_platform == 'win32'
pywin32; sys_platform == 'win32'
psutil
Pillow
1 change: 1 addition & 0 deletions src/main/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
11 changes: 11 additions & 0 deletions src/main/events/profile/process-profile-image.ts
Original file line number Diff line number Diff line change
@@ -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);
68 changes: 38 additions & 30 deletions src/main/events/profile/update-profile.ts
Original file line number Diff line number Diff line change
@@ -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 });
};
Expand Down
8 changes: 8 additions & 0 deletions src/main/services/download/python-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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 });
}
};

Expand Down Expand Up @@ -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>

<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,
}))}
/>

<Button disabled={isSaving} style={{ alignSelf: "end" }} type="submit">
{isSaving ? t("saving") : t("save")}
</Button>
</form>
<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 })}
/>

<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>
</SkeletonTheme>
);
};
35 changes: 27 additions & 8 deletions torrent-client/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import urllib.parse
import psutil
from torrent_downloader import TorrentDownloader
from profile_image_processor import ProfileImageProcessor

torrent_port = sys.argv[1]
http_port = sys.argv[2]
Expand Down Expand Up @@ -73,16 +74,30 @@ def do_GET(self):
def do_POST(self):
global torrent_downloader

if self.path == "/action":
if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401)
self.end_headers()
return
if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401)
self.end_headers()
return

content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))

if self.path == "/profile-image":
parsed_image_path = data['image_path']

try:
parsed_image_path, mime_type = ProfileImageProcessor.process_image(parsed_image_path)
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()

self.wfile.write(json.dumps({'imagePath': parsed_image_path, 'mimeType': mime_type}).encode('utf-8'))
except:
self.send_response(400)
self.end_headers()

elif self.path == "/action":
if torrent_downloader is None:
torrent_downloader = TorrentDownloader(torrent_port)

Expand All @@ -99,6 +114,10 @@ def do_POST(self):
self.send_response(200)
self.end_headers()

else:
self.send_response(404)
self.end_headers()


if __name__ == "__main__":
httpd = HTTPServer(("", int(http_port)), Handler)
Expand Down
Loading

0 comments on commit 797a09f

Please sign in to comment.