mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
feat: adding image processing
This commit is contained in:
commit
6e543fecb4
@ -4,7 +4,7 @@ import {
|
||||
defineConfig,
|
||||
loadEnv,
|
||||
swcPlugin,
|
||||
externalizeDepsPlugin
|
||||
externalizeDepsPlugin,
|
||||
} from "electron-vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||
@ -13,7 +13,7 @@ import svgr from "vite-plugin-svgr";
|
||||
var sentryPlugin = sentryVitePlugin({
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: "hydra-launcher",
|
||||
project: "hydra-launcher"
|
||||
project: "hydra-launcher",
|
||||
});
|
||||
var electron_vite_config_default = defineConfig(({ mode }) => {
|
||||
loadEnv(mode);
|
||||
@ -22,37 +22,35 @@ var electron_vite_config_default = defineConfig(({ mode }) => {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: ["better-sqlite3"]
|
||||
}
|
||||
external: ["better-sqlite3"],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@main": resolve("src/main"),
|
||||
"@locales": resolve("src/locales"),
|
||||
"@resources": resolve("resources"),
|
||||
"@shared": resolve("src/shared")
|
||||
}
|
||||
"@shared": resolve("src/shared"),
|
||||
},
|
||||
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin]
|
||||
},
|
||||
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin],
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
},
|
||||
renderer: {
|
||||
build: {
|
||||
sourcemap: true
|
||||
sourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@renderer": resolve("src/renderer/src"),
|
||||
"@locales": resolve("src/locales"),
|
||||
"@shared": resolve("src/shared")
|
||||
}
|
||||
"@shared": resolve("src/shared"),
|
||||
},
|
||||
},
|
||||
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin],
|
||||
},
|
||||
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin]
|
||||
}
|
||||
};
|
||||
});
|
||||
export {
|
||||
electron_vite_config_default as default
|
||||
};
|
||||
export { electron_vite_config_default as default };
|
||||
|
@ -54,7 +54,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",
|
||||
|
@ -4,3 +4,4 @@ cx_Logging; sys_platform == 'win32'
|
||||
lief; sys_platform == 'win32'
|
||||
pywin32; sys_platform == 'win32'
|
||||
psutil
|
||||
Pillow
|
||||
|
@ -286,6 +286,10 @@
|
||||
"friend_code_copied": "Friend code copied",
|
||||
"undo_friendship_modal_text": "This will undo your friendship with {{displayName}}",
|
||||
"privacy_hint": "To adjust who can see this, go to the <0>Settings</0>",
|
||||
"locked_profile": "This profile is private"
|
||||
"locked_profile": "This profile is private",
|
||||
"image_process_failure": "Failure while processing the image",
|
||||
"required_field": "This field is required",
|
||||
"displayname_min_length": "Display name must be at least 3 characters long",
|
||||
"displayname_max_length": "Display name must be at most 50 characters long"
|
||||
}
|
||||
}
|
||||
|
@ -289,6 +289,10 @@
|
||||
"friend_code_copied": "Código de amigo copiado",
|
||||
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}",
|
||||
"privacy_hint": "Pra controlar quem pode ver seu perfil, acesse a <0>Tela de Configurações</0>",
|
||||
"profile_locked": "Este perfil é privado"
|
||||
"profile_locked": "Este perfil é privado",
|
||||
"image_process_failure": "Falha ao processar a imagem",
|
||||
"required_field": "Este campo é obrigatório",
|
||||
"displayname_min_length": "Nome de exibição deve ter pelo menos 3 caracteres",
|
||||
"displayname_max_length": "Nome de exibição deve ter no máximo 50 caracteres"
|
||||
}
|
||||
}
|
||||
|
@ -277,6 +277,7 @@
|
||||
"pending": "Pendentes",
|
||||
"no_pending_invites": "Não tens convites de amizade pendentes",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,8 @@ import path from "node:path";
|
||||
|
||||
export const defaultDownloadsPath = app.getPath("downloads");
|
||||
|
||||
export const databasePath = path.join(
|
||||
app.getPath("appData"),
|
||||
"hydra",
|
||||
"hydra.db"
|
||||
);
|
||||
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||
export const databasePath = path.join(databaseDirectory, "hydra.db");
|
||||
|
||||
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
||||
|
||||
|
@ -54,6 +54,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 PythonInstance.processProfileImage(path);
|
||||
};
|
||||
|
||||
registerEvent("processProfileImage", processProfileImage);
|
@ -1,50 +1,54 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi, logger } 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 type { UpdateProfileRequest, UserProfile } from "@types";
|
||||
import { omit } from "lodash-es";
|
||||
import axios from "axios";
|
||||
|
||||
interface PresignedResponse {
|
||||
presignedUrl: string;
|
||||
profileImageUrl: string;
|
||||
}
|
||||
|
||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||
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: UpdateProfileRequest
|
||||
): 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((err) => {
|
||||
logger.error("Error uploading profile image", err);
|
||||
|
||||
return undefined;
|
||||
});
|
||||
const profileImageUrl = await getNewProfileImageUrl(
|
||||
updateProfile.profileImageUrl
|
||||
).catch(() => undefined);
|
||||
|
||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||
};
|
||||
|
@ -4,12 +4,14 @@ import updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import fs from "node:fs";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, PythonInstance, WindowManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import resources from "@locales";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { knexClient, migrationConfig } from "./knex-client";
|
||||
import { databaseDirectory } from "./constants";
|
||||
|
||||
const { autoUpdater } = updater;
|
||||
|
||||
@ -54,6 +56,10 @@ if (process.defaultApp) {
|
||||
}
|
||||
|
||||
const runMigrations = async () => {
|
||||
if (!fs.existsSync(databaseDirectory)) {
|
||||
fs.mkdirSync(databaseDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
await knexClient.migrate.list(migrationConfig).then((result) => {
|
||||
logger.log(
|
||||
"Migrations to run:",
|
||||
|
@ -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(
|
||||
|
@ -145,6 +145,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("undoFriendship", userId),
|
||||
updateProfile: (updateProfile: UpdateProfileRequest) =>
|
||||
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),
|
||||
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@ -150,6 +150,10 @@ declare global {
|
||||
updateProfile: (
|
||||
updateProfile: UpdateProfileRequest
|
||||
) => Promise<UserProfile>;
|
||||
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||
processProfileImage: (
|
||||
path: string
|
||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||
updateFriendRequest: (
|
||||
userId: string,
|
||||
|
@ -102,7 +102,7 @@ export function EditProfileModal(
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png"],
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -110,7 +110,14 @@ export function EditProfileModal(
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
|
||||
onChange(path);
|
||||
const { imagePath } = await window.electron
|
||||
.processProfileImage(path)
|
||||
.catch(() => {
|
||||
showErrorToast(t("image_process_failure"));
|
||||
return { imagePath: null };
|
||||
});
|
||||
|
||||
onChange(imagePath);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ import json
|
||||
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]
|
||||
@ -73,7 +74,6 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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()
|
||||
@ -83,6 +83,21 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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)
|
||||
|
||||
@ -99,6 +114,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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)
|
@ -4347,15 +4347,6 @@ file-type@^18.7.0:
|
||||
strtok3 "^7.0.0"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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