mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
feat: adding profile picture background
This commit is contained in:
parent
b3e2346808
commit
79ca354da1
@ -7,6 +7,7 @@ import {
|
|||||||
useAppSelector,
|
useAppSelector,
|
||||||
useDownload,
|
useDownload,
|
||||||
useLibrary,
|
useLibrary,
|
||||||
|
useUserDetails,
|
||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./app.css";
|
import * as styles from "./app.css";
|
||||||
@ -19,7 +20,6 @@ import {
|
|||||||
toggleDraggingDisabled,
|
toggleDraggingDisabled,
|
||||||
closeToast,
|
closeToast,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { useUserAuth } from "./hooks/use-user-auth";
|
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -31,7 +31,7 @@ export function App() {
|
|||||||
|
|
||||||
const { clearDownload, setLastPacket } = useDownload();
|
const { clearDownload, setLastPacket } = useDownload();
|
||||||
|
|
||||||
const { updateUserAuth, clearUserAuth } = useUserAuth();
|
const { updateUser, clearUser } = useUserDetails();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -39,9 +39,11 @@ export function App() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const search = useAppSelector((state) => state.search.value);
|
const search = useAppSelector((state) => state.search.value);
|
||||||
|
|
||||||
const draggingDisabled = useAppSelector(
|
const draggingDisabled = useAppSelector(
|
||||||
(state) => state.window.draggingDisabled
|
(state) => state.window.draggingDisabled
|
||||||
);
|
);
|
||||||
|
|
||||||
const toast = useAppSelector((state) => state.toast);
|
const toast = useAppSelector((state) => state.toast);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -70,20 +72,24 @@ export function App() {
|
|||||||
};
|
};
|
||||||
}, [clearDownload, setLastPacket, updateLibrary]);
|
}, [clearDownload, setLastPacket, updateLibrary]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateUser();
|
||||||
|
}, [updateUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listeners = [
|
const listeners = [
|
||||||
window.electron.onSignIn(() => {
|
window.electron.onSignIn(() => {
|
||||||
updateUserAuth();
|
updateUser();
|
||||||
}),
|
}),
|
||||||
window.electron.onSignOut(() => {
|
window.electron.onSignOut(() => {
|
||||||
clearUserAuth();
|
clearUser();
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
listeners.forEach((unsubscribe) => unsubscribe());
|
listeners.forEach((unsubscribe) => unsubscribe());
|
||||||
};
|
};
|
||||||
}, [clearUserAuth, updateUserAuth]);
|
}, [updateUser, clearUser]);
|
||||||
|
|
||||||
const handleSearch = useCallback(
|
const handleSearch = useCallback(
|
||||||
(query: string) => {
|
(query: string) => {
|
||||||
|
@ -1,24 +1,28 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonIcon } from "@primer/octicons-react";
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { useUserAuth } from "@renderer/hooks/use-user-auth";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function SidebarProfile() {
|
export function SidebarProfile() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { userAuth, isLoading } = useUserAuth();
|
const { userDetails, profileBackground } = useUserDetails();
|
||||||
|
|
||||||
const handleClickProfile = () => {
|
const handleClickProfile = () => {
|
||||||
navigate(`/user/${userAuth!.id}`);
|
navigate(`/user/${userDetails!.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickLogin = () => {
|
const handleClickLogin = () => {
|
||||||
window.electron.openExternal("https://auth.hydra.losbroxas.org");
|
window.electron.openExternal("https://auth.hydra.losbroxas.org");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return null;
|
const profileButtonBackground = useMemo(() => {
|
||||||
|
if (profileBackground) return profileBackground;
|
||||||
|
return undefined;
|
||||||
|
}, [profileBackground]);
|
||||||
|
|
||||||
if (userAuth == null) {
|
if (userDetails == null) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@ -43,14 +47,15 @@ export function SidebarProfile() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.profileButton}
|
className={styles.profileButton}
|
||||||
|
style={{ background: profileButtonBackground }}
|
||||||
onClick={handleClickProfile}
|
onClick={handleClickProfile}
|
||||||
>
|
>
|
||||||
<div className={styles.profileAvatar}>
|
<div className={styles.profileAvatar}>
|
||||||
{userAuth.profileImageUrl ? (
|
{userDetails.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatar}
|
className={styles.profileAvatar}
|
||||||
src={userAuth.profileImageUrl}
|
src={userDetails.profileImageUrl}
|
||||||
alt={userAuth.displayName}
|
alt={userDetails.displayName}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PersonIcon />
|
<PersonIcon />
|
||||||
@ -58,7 +63,7 @@ export function SidebarProfile() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.profileButtonInformation}>
|
<div className={styles.profileButtonInformation}>
|
||||||
<p style={{ fontWeight: "bold" }}>{userAuth.displayName}</p>
|
<p style={{ fontWeight: "bold" }}>{userDetails.displayName}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
@ -149,7 +149,9 @@ export const profileAvatar = style({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
objectFit: "cover",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileButtonInformation = style({
|
export const profileButtonInformation = style({
|
||||||
|
@ -4,4 +4,4 @@ export * from "./use-preferences-slice";
|
|||||||
export * from "./download-slice";
|
export * from "./download-slice";
|
||||||
export * from "./window-slice";
|
export * from "./window-slice";
|
||||||
export * from "./toast-slice";
|
export * from "./toast-slice";
|
||||||
export * from "./user-auth-slice";
|
export * from "./user-details-slice";
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
|
||||||
import type { UserAuth } from "@types";
|
|
||||||
|
|
||||||
export interface UserAuthState {
|
|
||||||
userAuth: UserAuth | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: UserAuthState = {
|
|
||||||
userAuth: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userAuthSlice = createSlice({
|
|
||||||
name: "user-auth",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setUserAuth: (state, userAuth: PayloadAction<UserAuth | null>) => {
|
|
||||||
state.userAuth = userAuth.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setUserAuth } = userAuthSlice.actions;
|
|
32
src/renderer/src/features/user-details-slice.ts
Normal file
32
src/renderer/src/features/user-details-slice.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
|
import type { UserDetails } from "@types";
|
||||||
|
|
||||||
|
export interface UserDetailsState {
|
||||||
|
userDetails: UserDetails | null;
|
||||||
|
profileBackground: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: UserDetailsState = {
|
||||||
|
userDetails: null,
|
||||||
|
profileBackground: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userDetailsSlice = createSlice({
|
||||||
|
name: "user-details",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setUserDetails: (state, action: PayloadAction<UserDetails>) => {
|
||||||
|
state.userDetails = action.payload;
|
||||||
|
},
|
||||||
|
setProfileBackground: (state, action: PayloadAction<string>) => {
|
||||||
|
state.profileBackground = action.payload;
|
||||||
|
},
|
||||||
|
clearUserDetails: (state) => {
|
||||||
|
state.userDetails = null;
|
||||||
|
state.profileBackground = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setUserDetails, setProfileBackground, clearUserDetails } =
|
||||||
|
userDetailsSlice.actions;
|
@ -3,3 +3,4 @@ export * from "./use-library";
|
|||||||
export * from "./use-date";
|
export * from "./use-date";
|
||||||
export * from "./use-toast";
|
export * from "./use-toast";
|
||||||
export * from "./redux";
|
export * from "./redux";
|
||||||
|
export * from "./use-user-details";
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { useAppDispatch, useAppSelector } from "./redux";
|
|
||||||
import { setUserAuth } from "@renderer/features";
|
|
||||||
|
|
||||||
export function useUserAuth() {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
const { userAuth } = useAppSelector((state) => state.userAuth);
|
|
||||||
|
|
||||||
const signOut = useCallback(async () => {
|
|
||||||
dispatch(setUserAuth(null));
|
|
||||||
return window.electron.signOut();
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const updateUserAuth = useCallback(async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
return window.electron
|
|
||||||
.getMe()
|
|
||||||
.then((userAuth) => dispatch(setUserAuth(userAuth)))
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateUserAuth();
|
|
||||||
}, [updateUserAuth]);
|
|
||||||
|
|
||||||
const clearUserAuth = useCallback(async () => {
|
|
||||||
dispatch(setUserAuth(null));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return { userAuth, isLoading, updateUserAuth, signOut, clearUserAuth };
|
|
||||||
}
|
|
57
src/renderer/src/hooks/use-user-details.ts
Normal file
57
src/renderer/src/hooks/use-user-details.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { average } from "color.js";
|
||||||
|
|
||||||
|
import { useAppDispatch, useAppSelector } from "./redux";
|
||||||
|
import {
|
||||||
|
clearUserDetails,
|
||||||
|
setProfileBackground,
|
||||||
|
setUserDetails,
|
||||||
|
} from "@renderer/features";
|
||||||
|
import { darkenColor } from "@renderer/helpers";
|
||||||
|
|
||||||
|
export function useUserDetails() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { userDetails, profileBackground } = useAppSelector(
|
||||||
|
(state) => state.userDetails
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearUser = useCallback(async () => {
|
||||||
|
dispatch(clearUserDetails());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const signOut = useCallback(async () => {
|
||||||
|
clearUser();
|
||||||
|
|
||||||
|
return window.electron.signOut();
|
||||||
|
}, [clearUser]);
|
||||||
|
|
||||||
|
const updateUser = useCallback(async () => {
|
||||||
|
return window.electron.getMe().then(async (userDetails) => {
|
||||||
|
if (userDetails) {
|
||||||
|
dispatch(setUserDetails(userDetails));
|
||||||
|
|
||||||
|
if (userDetails.profileImageUrl) {
|
||||||
|
const output = await average(userDetails.profileImageUrl, {
|
||||||
|
amount: 1,
|
||||||
|
format: "hex",
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setProfileBackground(
|
||||||
|
`linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.7)})`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userDetails,
|
||||||
|
updateUser,
|
||||||
|
signOut,
|
||||||
|
clearUser,
|
||||||
|
profileBackground,
|
||||||
|
};
|
||||||
|
}
|
@ -1,32 +1,27 @@
|
|||||||
import { UserGame, UserProfile } from "@types";
|
import { UserGame, UserProfile } from "@types";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { average } from "color.js";
|
|
||||||
|
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { useDate } from "@renderer/hooks";
|
import { useDate, useUserDetails } from "@renderer/hooks";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { buildGameDetailsPath, darkenColor } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonIcon } from "@primer/octicons-react";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useUserAuth } from "@renderer/hooks/use-user-auth";
|
|
||||||
|
|
||||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
export interface ProfileContentProps {
|
export interface ProfileContentProps {
|
||||||
userProfile: UserProfile;
|
userProfile: UserProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserContent = ({ userProfile }: ProfileContentProps) => {
|
export function UserContent({ userProfile }: ProfileContentProps) {
|
||||||
const { t, i18n } = useTranslation("user_profile");
|
const { t, i18n } = useTranslation("user_profile");
|
||||||
|
|
||||||
const { userAuth, signOut } = useUserAuth();
|
const { userDetails, profileBackground, signOut } = useUserDetails();
|
||||||
|
|
||||||
const profileImageRef = useRef<HTMLImageElement | null>(null);
|
|
||||||
|
|
||||||
const [backgroundColors, setBackgroundColors] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -64,34 +59,28 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => {
|
|||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAvatarLoad = async () => {
|
const isMe = userDetails?.id == userProfile.id;
|
||||||
const output = await average(profileImageRef.current!, {
|
|
||||||
amount: 1,
|
|
||||||
format: "hex",
|
|
||||||
});
|
|
||||||
|
|
||||||
setBackgroundColors([
|
const profileContentBoxBackground = useMemo(() => {
|
||||||
darkenColor(output as string, 0.6),
|
if (profileBackground) return profileBackground;
|
||||||
darkenColor(output as string, 0.7),
|
/* TODO: Render background colors for other users */
|
||||||
]);
|
return undefined;
|
||||||
};
|
}, [profileBackground]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section
|
<section
|
||||||
className={styles.profileContentBox}
|
className={styles.profileContentBox}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(135deg, ${backgroundColors[0]}, ${backgroundColors[1]})`,
|
background: profileContentBoxBackground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.profileAvatarContainer}>
|
<div className={styles.profileAvatarContainer}>
|
||||||
{userProfile.profileImageUrl ? (
|
{userProfile.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
ref={profileImageRef}
|
|
||||||
className={styles.profileAvatar}
|
className={styles.profileAvatar}
|
||||||
alt={userProfile.displayName}
|
alt={userProfile.displayName}
|
||||||
src={userProfile.profileImageUrl}
|
src={userProfile.profileImageUrl}
|
||||||
onLoad={handleAvatarLoad}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PersonIcon size={72} />
|
<PersonIcon size={72} />
|
||||||
@ -102,7 +91,7 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => {
|
|||||||
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
|
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{userAuth && userAuth.id == userProfile.id && (
|
{isMe && (
|
||||||
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
|
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
|
||||||
<Button theme="danger" onClick={handleSignout}>
|
<Button theme="danger" onClick={handleSignout}>
|
||||||
{t("sign_out")}
|
{t("sign_out")}
|
||||||
@ -213,4 +202,4 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
searchSlice,
|
searchSlice,
|
||||||
userPreferencesSlice,
|
userPreferencesSlice,
|
||||||
toastSlice,
|
toastSlice,
|
||||||
userAuthSlice,
|
userDetailsSlice,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
@ -17,7 +17,7 @@ export const store = configureStore({
|
|||||||
userPreferences: userPreferencesSlice.reducer,
|
userPreferences: userPreferencesSlice.reducer,
|
||||||
download: downloadSlice.reducer,
|
download: downloadSlice.reducer,
|
||||||
toast: toastSlice.reducer,
|
toast: toastSlice.reducer,
|
||||||
userAuth: userAuthSlice.reducer,
|
userDetails: userDetailsSlice.reducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -244,7 +244,7 @@ export interface RealDebridUser {
|
|||||||
expiration: string;
|
expiration: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserAuth {
|
export interface UserDetails {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
|
Loading…
Reference in New Issue
Block a user