mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge pull request #952 from hydralauncher/feat/process-profile-image
feat: process profile image
This commit is contained in:
commit
863a3b7d1f
@ -53,7 +53,6 @@
|
|||||||
"electron-log": "^5.1.4",
|
"electron-log": "^5.1.4",
|
||||||
"electron-updater": "^6.1.8",
|
"electron-updater": "^6.1.8",
|
||||||
"fetch-cookie": "^3.0.1",
|
"fetch-cookie": "^3.0.1",
|
||||||
"file-type": "^19.0.0",
|
|
||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.7.43",
|
||||||
"i18next": "^23.11.2",
|
"i18next": "^23.11.2",
|
||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
|
@ -4,3 +4,4 @@ cx_Logging; sys_platform == 'win32'
|
|||||||
lief; sys_platform == 'win32'
|
lief; sys_platform == 'win32'
|
||||||
pywin32; sys_platform == 'win32'
|
pywin32; sys_platform == 'win32'
|
||||||
psutil
|
psutil
|
||||||
|
Pillow
|
||||||
|
@ -273,6 +273,7 @@
|
|||||||
"no_pending_invites": "You have no pending invites",
|
"no_pending_invites": "You have no pending invites",
|
||||||
"no_blocked_users": "You have no blocked users",
|
"no_blocked_users": "You have no blocked users",
|
||||||
"friend_code_copied": "Friend code copied",
|
"friend_code_copied": "Friend code copied",
|
||||||
"undo_friendship_modal_text": "This will undo your friendship with {{displayName}}"
|
"undo_friendship_modal_text": "This will undo your friendship with {{displayName}}",
|
||||||
|
"image_process_failure": "Failure while processing the image"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,6 +276,7 @@
|
|||||||
"no_pending_invites": "Você não possui convites de amizade pendentes",
|
"no_pending_invites": "Você não possui convites de amizade pendentes",
|
||||||
"no_blocked_users": "Você não tem nenhum usuário bloqueado",
|
"no_blocked_users": "Você não tem nenhum usuário bloqueado",
|
||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}"
|
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}",
|
||||||
|
"image_process_failure": "Falha ao processar a imagem"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,6 +277,7 @@
|
|||||||
"pending": "Pendentes",
|
"pending": "Pendentes",
|
||||||
"no_pending_invites": "Não tens convites de amizade pendentes",
|
"no_pending_invites": "Não tens convites de amizade pendentes",
|
||||||
"no_blocked_users": "Não tens nenhum utilizador bloqueado",
|
"no_blocked_users": "Não tens nenhum utilizador bloqueado",
|
||||||
"friend_code_copied": "Código de amigo copiado"
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
|
"image_process_failure": "Falha ao processar a imagem"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ import "./profile/get-me";
|
|||||||
import "./profile/undo-friendship";
|
import "./profile/undo-friendship";
|
||||||
import "./profile/update-friend-request";
|
import "./profile/update-friend-request";
|
||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
|
import "./profile/process-profile-image";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
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 PythonInstance.processProfileImage(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("processProfileImage", processProfileImage);
|
@ -1,46 +1,54 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi, PythonInstance } from "@main/services";
|
||||||
import axios from "axios";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import type { UpdateProfileProps, UserProfile } from "@types";
|
||||||
import { UpdateProfileProps, UserProfile } from "@types";
|
import { omit } from "lodash-es";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
interface PresignedResponse {
|
||||||
|
presignedUrl: string;
|
||||||
|
profileImageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
|
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 (
|
const updateProfile = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
updateProfile: UpdateProfileProps
|
updateProfile: UpdateProfileProps
|
||||||
): Promise<UserProfile> => {
|
) => {
|
||||||
if (!updateProfile.profileImageUrl) {
|
if (!updateProfile.profileImageUrl) {
|
||||||
return patchUserProfile(updateProfile);
|
return patchUserProfile(omit(updateProfile, "profileImageUrl"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const newProfileImagePath = updateProfile.profileImageUrl;
|
const profileImageUrl = await getNewProfileImageUrl(
|
||||||
|
updateProfile.profileImageUrl
|
||||||
const stats = fs.statSync(newProfileImagePath);
|
).catch(() => undefined);
|
||||||
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);
|
|
||||||
|
|
||||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||||
};
|
};
|
||||||
|
@ -166,6 +166,14 @@ export class PythonInstance {
|
|||||||
this.downloadingGameId = -1;
|
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) {
|
private static async handleRpcError(_error: unknown) {
|
||||||
await this.rpc.get("/healthcheck").catch(() => {
|
await this.rpc.get("/healthcheck").catch(() => {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@ -141,6 +141,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("undoFriendship", userId),
|
ipcRenderer.invoke("undoFriendship", userId),
|
||||||
updateProfile: (updateProfile: UpdateProfileProps) =>
|
updateProfile: (updateProfile: UpdateProfileProps) =>
|
||||||
ipcRenderer.invoke("updateProfile", updateProfile),
|
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||||
|
processProfileImage: (imagePath: string) =>
|
||||||
|
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
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>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
undoFriendship: (userId: string) => Promise<void>;
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||||
|
processProfileImage: (
|
||||||
|
path: string
|
||||||
|
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
updateFriendRequest: (
|
updateFriendRequest: (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -5,7 +5,8 @@ import { UserProfile } from "@types";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "../user.css";
|
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 {
|
export interface UserEditProfileProps {
|
||||||
userProfile: UserProfile;
|
userProfile: UserProfile;
|
||||||
@ -21,9 +22,10 @@ export const UserEditProfile = ({
|
|||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
displayName: userProfile.displayName,
|
displayName: userProfile.displayName,
|
||||||
profileVisibility: userProfile.profileVisibility,
|
profileVisibility: userProfile.profileVisibility,
|
||||||
imageProfileUrl: null as string | null,
|
profileImageUrl: null as string | null,
|
||||||
});
|
});
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isLoadingImage, setIsLoadingImage] = useState(false);
|
||||||
|
|
||||||
const { patchUser } = useUserDetails();
|
const { patchUser } = useUserDetails();
|
||||||
|
|
||||||
@ -53,9 +55,17 @@ export const UserEditProfile = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (filePaths && filePaths.length > 0) {
|
if (filePaths && filePaths.length > 0) {
|
||||||
const path = filePaths[0];
|
setIsLoadingImage(true);
|
||||||
|
|
||||||
setForm({ ...form, imageProfileUrl: path });
|
const { imagePath } = await window.electron
|
||||||
|
.processProfileImage(filePaths[0])
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("image_process_failure"));
|
||||||
|
return { imagePath: null };
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoadingImage(false));
|
||||||
|
|
||||||
|
setForm({ ...form, profileImageUrl: imagePath });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,13 +95,32 @@ export const UserEditProfile = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const avatarUrl = useMemo(() => {
|
const profileImageUrl = useMemo(() => {
|
||||||
if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`;
|
if (form.profileImageUrl) return `local:${form.profileImageUrl}`;
|
||||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||||
return null;
|
return null;
|
||||||
}, [form, userProfile]);
|
}, [form, userProfile]);
|
||||||
|
|
||||||
|
const profileImageContent = () => {
|
||||||
|
if (isLoadingImage) {
|
||||||
|
return <Skeleton className={styles.profileAvatar} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileImageUrl) {
|
||||||
return (
|
return (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={userProfile.displayName}
|
||||||
|
src={profileImageUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PersonIcon size={96} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSaveProfile}
|
onSubmit={handleSaveProfile}
|
||||||
style={{
|
style={{
|
||||||
@ -107,15 +136,7 @@ export const UserEditProfile = ({
|
|||||||
className={styles.profileAvatarEditContainer}
|
className={styles.profileAvatarEditContainer}
|
||||||
onClick={handleChangeProfileAvatar}
|
onClick={handleChangeProfileAvatar}
|
||||||
>
|
>
|
||||||
{avatarUrl ? (
|
{profileImageContent()}
|
||||||
<img
|
|
||||||
className={styles.profileAvatar}
|
|
||||||
alt={userProfile.displayName}
|
|
||||||
src={avatarUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={96} />
|
|
||||||
)}
|
|
||||||
<div className={styles.editProfileImageBadge}>
|
<div className={styles.editProfileImageBadge}>
|
||||||
<DeviceCameraIcon size={16} />
|
<DeviceCameraIcon size={16} />
|
||||||
</div>
|
</div>
|
||||||
@ -146,5 +167,6 @@ export const UserEditProfile = ({
|
|||||||
{isSaving ? t("saving") : t("save")}
|
{isSaving ? t("saving") : t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</SkeletonTheme>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import json
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import psutil
|
import psutil
|
||||||
from torrent_downloader import TorrentDownloader
|
from torrent_downloader import TorrentDownloader
|
||||||
|
from profile_image_processor import ProfileImageProcessor
|
||||||
|
|
||||||
torrent_port = sys.argv[1]
|
torrent_port = sys.argv[1]
|
||||||
http_port = sys.argv[2]
|
http_port = sys.argv[2]
|
||||||
@ -73,7 +74,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
global torrent_downloader
|
global torrent_downloader
|
||||||
|
|
||||||
if self.path == "/action":
|
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
if self.headers.get(self.rpc_password_header) != rpc_password:
|
||||||
self.send_response(401)
|
self.send_response(401)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@ -83,6 +83,21 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
post_data = self.rfile.read(content_length)
|
post_data = self.rfile.read(content_length)
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
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:
|
if torrent_downloader is None:
|
||||||
torrent_downloader = TorrentDownloader(torrent_port)
|
torrent_downloader = TorrentDownloader(torrent_port)
|
||||||
|
|
||||||
@ -99,6 +114,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
httpd = HTTPServer(("", int(http_port)), Handler)
|
httpd = HTTPServer(("", int(http_port)), Handler)
|
||||||
|
31
torrent-client/profile_image_processor.py
Normal file
31
torrent-client/profile_image_processor.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import tempfile
|
||||||
|
import os, uuid
|
||||||
|
|
||||||
|
class ProfileImageProcessor:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_parsed_image_data(image_path):
|
||||||
|
Image.MAX_IMAGE_PIXELS = 933120000
|
||||||
|
|
||||||
|
image = Image.open(image_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
image.seek(1)
|
||||||
|
except EOFError:
|
||||||
|
mime_type = image.get_format_mimetype()
|
||||||
|
return image_path, mime_type
|
||||||
|
else:
|
||||||
|
newUUID = str(uuid.uuid4())
|
||||||
|
new_image_path = os.path.join(tempfile.gettempdir(), newUUID) + ".webp"
|
||||||
|
image.save(new_image_path)
|
||||||
|
|
||||||
|
new_image = Image.open(new_image_path)
|
||||||
|
mime_type = new_image.get_format_mimetype()
|
||||||
|
|
||||||
|
return new_image_path, mime_type
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def process_image(image_path):
|
||||||
|
return ProfileImageProcessor.get_parsed_image_data(image_path)
|
@ -4342,15 +4342,6 @@ file-type@^18.7.0:
|
|||||||
strtok3 "^7.0.0"
|
strtok3 "^7.0.0"
|
||||||
token-types "^5.0.1"
|
token-types "^5.0.1"
|
||||||
|
|
||||||
file-type@^19.0.0:
|
|
||||||
version "19.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/file-type/-/file-type-19.0.0.tgz"
|
|
||||||
integrity sha512-s7cxa7/leUWLiXO78DVVfBVse+milos9FitauDLG1pI7lNaJ2+5lzPnr2N24ym+84HVwJL6hVuGfgVE+ALvU8Q==
|
|
||||||
dependencies:
|
|
||||||
readable-web-to-node-stream "^3.0.2"
|
|
||||||
strtok3 "^7.0.0"
|
|
||||||
token-types "^5.0.1"
|
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user