feat: adding local file protocol

This commit is contained in:
Chubby Granny Chaser 2024-06-19 09:48:11 +01:00
parent 1ef8e3fce3
commit 1fb1c9e81a
No known key found for this signature in database
10 changed files with 42 additions and 54 deletions

View File

@ -24,7 +24,6 @@ import "./library/remove-game";
import "./library/remove-game-from-library"; import "./library/remove-game-from-library";
import "./misc/open-external"; import "./misc/open-external";
import "./misc/show-open-dialog"; import "./misc/show-open-dialog";
import "./misc/image-path-to-base-64";
import "./torrenting/cancel-game-download"; import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download"; import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download"; import "./torrenting/resume-game-download";

View File

@ -1,14 +0,0 @@
import mime from "mime";
import { registerEvent } from "../register-event";
import fs from "node:fs";
const imagePathToBase64 = async (
_event: Electron.IpcMainInvokeEvent,
filePath: string
) => {
const buffer = fs.readFileSync(filePath);
const mimeType = mime.getType(filePath);
return `data:${mimeType};base64,${buffer.toString("base64")}`;
};
registerEvent("imagePathToBase64", imagePathToBase64);

View File

@ -2,6 +2,7 @@ import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services/hydra-api"; import { HydraApi } from "@main/services/hydra-api";
import axios from "axios"; import axios from "axios";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path";
import mime from "mime"; import mime from "mime";
import { UserProfile } from "@types"; import { UserProfile } from "@types";
@ -26,6 +27,8 @@ const updateProfile = async (
displayName: string, displayName: string,
newProfileImagePath: string | null newProfileImagePath: string | null
): Promise<UserProfile> => { ): Promise<UserProfile> => {
console.log(newProfileImagePath);
if (!newProfileImagePath) { if (!newProfileImagePath) {
return (await patchUserProfile(displayName)).data; return (await patchUserProfile(displayName)).data;
} }
@ -35,7 +38,7 @@ const updateProfile = async (
const fileSizeInBytes = stats.size; const fileSizeInBytes = stats.size;
const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, { const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, {
imageExt: newProfileImagePath.split(".").at(-1), imageExt: path.extname(newProfileImagePath).slice(1),
imageLength: fileSizeInBytes, imageLength: fileSizeInBytes,
}) })
.then(async (preSignedResponse) => { .then(async (preSignedResponse) => {

View File

@ -2,6 +2,7 @@ import { app, BrowserWindow, net, protocol } from "electron";
import updater from "electron-updater"; import updater from "electron-updater";
import i18n from "i18next"; import i18n from "i18next";
import path from "node:path"; import path from "node:path";
import url from "node:url";
import { electronApp, optimizer } from "@electron-toolkit/utils"; import { electronApp, optimizer } from "@electron-toolkit/utils";
import { DownloadManager, logger, WindowManager } from "@main/services"; import { DownloadManager, logger, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
@ -51,9 +52,10 @@ if (process.defaultApp) {
app.whenReady().then(async () => { app.whenReady().then(async () => {
electronApp.setAppUserModelId("site.hydralauncher.hydra"); electronApp.setAppUserModelId("site.hydralauncher.hydra");
protocol.handle("hydra", (request) => protocol.handle("local", (request) => {
net.fetch("file://" + request.url.slice("hydra://".length)) const filePath = request.url.slice("local://".length);
); return net.fetch(url.pathToFileURL(filePath).toString());
});
await dataSource.initialize(); await dataSource.initialize();
await dataSource.runMigrations(); await dataSource.runMigrations();

View File

@ -106,8 +106,6 @@ contextBridge.exposeInMainWorld("electron", {
getVersion: () => ipcRenderer.invoke("getVersion"), getVersion: () => ipcRenderer.invoke("getVersion"),
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
imagePathToBase64: (filePath: string) =>
ipcRenderer.invoke("imagePathToBase64", filePath),
showOpenDialog: (options: Electron.OpenDialogOptions) => showOpenDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke("showOpenDialog", options), ipcRenderer.invoke("showOpenDialog", options),
platform: process.platform, platform: process.platform,

View File

@ -6,7 +6,7 @@
<title>Hydra</title> <title>Hydra</title>
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.losbroxas.org https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://cdn.losbroxas.org https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://cdn.losbroxas.org https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://cdn.losbroxas.org https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
/> />
</head> </head>
<body style="background-color: #1c1c1c"> <body style="background-color: #1c1c1c">

View File

@ -96,7 +96,6 @@ declare global {
/* Misc */ /* Misc */
openExternal: (src: string) => Promise<void>; openExternal: (src: string) => Promise<void>;
imagePathToBase64: (filePath: string) => Promise<string>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
ping: () => string; ping: () => string;
getDefaultDownloadsPath: () => Promise<string>; getDefaultDownloadsPath: () => Promise<string>;

View File

@ -17,7 +17,7 @@ const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
export interface ProfileContentProps { export interface ProfileContentProps {
userProfile: UserProfile; userProfile: UserProfile;
updateUserProfile: () => void; updateUserProfile: () => Promise<void>;
} }
export function UserContent({ export function UserContent({
@ -70,10 +70,6 @@ export function UserContent({
navigate("/"); navigate("/");
}; };
const handleUpdateUserProfile = async () => {
updateUserProfile();
};
const isMe = userDetails?.id == userProfile.id; const isMe = userDetails?.id == userProfile.id;
const profileContentBoxBackground = useMemo(() => { const profileContentBoxBackground = useMemo(() => {
@ -87,7 +83,7 @@ export function UserContent({
<UserEditProfileModal <UserEditProfileModal
visible={showEditProfileModal} visible={showEditProfileModal}
onClose={() => setShowEditProfileModal(false)} onClose={() => setShowEditProfileModal(false)}
updateUser={handleUpdateUserProfile} updateUserProfile={updateUserProfile}
userProfile={userProfile} userProfile={userProfile}
/> />

View File

@ -1,33 +1,36 @@
import { Button, Modal, TextField } from "@renderer/components"; import { Button, Modal, TextField } from "@renderer/components";
import { UserProfile } from "@types"; import { UserProfile } from "@types";
import * as styles from "./user.css"; import * as styles from "./user.css";
import { PencilIcon, PersonIcon } from "@primer/octicons-react"; import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useToast, useUserDetails } from "@renderer/hooks"; import { useToast, useUserDetails } from "@renderer/hooks";
export interface UserEditProfileModalProps { export interface UserEditProfileModalProps {
userProfile: UserProfile; userProfile: UserProfile;
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
updateUser: () => Promise<void>; updateUserProfile: () => Promise<void>;
} }
export const UserEditProfileModal = ({ export const UserEditProfileModal = ({
userProfile, userProfile,
visible, visible,
onClose, onClose,
updateUser, updateUserProfile,
}: UserEditProfileModalProps) => { }: UserEditProfileModalProps) => {
const [displayName, setDisplayName] = useState(userProfile.displayName); const [displayName, setDisplayName] = useState("");
const [newImagePath, setNewImagePath] = useState<string | null>(null); const [newImagePath, setNewImagePath] = useState<string | null>(null);
const [newImageBase64, setNewImageBase64] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const { patchUser } = useUserDetails(); const { patchUser } = useUserDetails();
const { showSuccessToast, showErrorToast } = useToast(); const { showSuccessToast, showErrorToast } = useToast();
useEffect(() => {
setDisplayName(userProfile.displayName);
}, [userProfile.displayName]);
const handleChangeProfileAvatar = async () => { const handleChangeProfileAvatar = async () => {
const { filePaths } = await window.electron.showOpenDialog({ const { filePaths } = await window.electron.showOpenDialog({
properties: ["openFile"], properties: ["openFile"],
@ -42,19 +45,16 @@ export const UserEditProfileModal = ({
if (filePaths && filePaths.length > 0) { if (filePaths && filePaths.length > 0) {
const path = filePaths[0]; const path = filePaths[0];
window.electron.imagePathToBase64(path).then((base64) => {
setNewImageBase64(base64);
});
setNewImagePath(path); setNewImagePath(path);
} }
}; };
const handleSaveProfile = async () => { const handleSaveProfile = async () => {
setIsSaving(true); setIsSaving(true);
patchUser(displayName, newImagePath) patchUser(displayName, newImagePath)
.then(() => { .then(async () => {
updateUser(); await updateUserProfile();
showSuccessToast("Salvo com sucesso"); showSuccessToast("Salvo com sucesso");
cleanFormAndClose(); cleanFormAndClose();
}) })
@ -69,7 +69,6 @@ export const UserEditProfileModal = ({
const resetModal = () => { const resetModal = () => {
setDisplayName(userProfile.displayName); setDisplayName(userProfile.displayName);
setNewImagePath(null); setNewImagePath(null);
setNewImageBase64(null);
}; };
const cleanFormAndClose = () => { const cleanFormAndClose = () => {
@ -77,6 +76,12 @@ export const UserEditProfileModal = ({
onClose(); onClose();
}; };
const avatarUrl = useMemo(() => {
if (newImagePath) return `local:${newImagePath}`;
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
return null;
}, [newImagePath, userProfile.profileImageUrl]);
return ( return (
<> <>
<Modal <Modal
@ -84,7 +89,8 @@ export const UserEditProfileModal = ({
title="Editar Perfil" title="Editar Perfil"
onClose={cleanFormAndClose} onClose={cleanFormAndClose}
> >
<section <form
onSubmit={handleSaveProfile}
style={{ style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -95,20 +101,21 @@ export const UserEditProfileModal = ({
}} }}
> >
<button <button
type="button"
className={styles.profileAvatarEditContainer} className={styles.profileAvatarEditContainer}
onClick={handleChangeProfileAvatar} onClick={handleChangeProfileAvatar}
> >
{newImageBase64 || userProfile.profileImageUrl ? ( {avatarUrl ? (
<img <img
className={styles.profileAvatar} className={styles.profileAvatar}
alt={userProfile.displayName} alt={userProfile.displayName}
src={newImageBase64 ?? userProfile.profileImageUrl ?? ""} src={avatarUrl}
/> />
) : ( ) : (
<PersonIcon size={96} /> <PersonIcon size={96} />
)} )}
<div className={styles.editProfileImageBadge}> <div className={styles.editProfileImageBadge}>
<PencilIcon size={16} /> <DeviceCameraIcon size={16} />
</div> </div>
</button> </button>
@ -121,11 +128,11 @@ export const UserEditProfileModal = ({
<Button <Button
disabled={isSaving} disabled={isSaving}
style={{ alignSelf: "end" }} style={{ alignSelf: "end" }}
onClick={handleSaveProfile} type="submit"
> >
{isSaving ? "Salvando..." : "Salvar"} {isSaving ? "Salvando" : "Salvar"}
</Button> </Button>
</section> </form>
</Modal> </Modal>
</> </>
); );

View File

@ -16,7 +16,7 @@ export const User = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const getUserProfile = useCallback(() => { const getUserProfile = useCallback(() => {
window.electron.getUser(userId!).then((userProfile) => { return window.electron.getUser(userId!).then((userProfile) => {
if (userProfile) { if (userProfile) {
dispatch(setHeaderTitle(userProfile.displayName)); dispatch(setHeaderTitle(userProfile.displayName));
setUserProfile(userProfile); setUserProfile(userProfile);
@ -28,9 +28,7 @@ export const User = () => {
getUserProfile(); getUserProfile();
}, [getUserProfile]); }, [getUserProfile]);
const handleUpdateProfile = () => { console.log(userProfile);
getUserProfile();
};
return ( return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444"> <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
@ -38,7 +36,7 @@ export const User = () => {
{userProfile ? ( {userProfile ? (
<UserContent <UserContent
userProfile={userProfile} userProfile={userProfile}
updateUserProfile={handleUpdateProfile} updateUserProfile={getUserProfile}
/> />
) : ( ) : (
<UserSkeleton /> <UserSkeleton />