feat: adding profile picture background

This commit is contained in:
Chubby Granny Chaser 2024-06-18 00:09:26 +01:00
parent b3e2346808
commit 79ca354da1
No known key found for this signature in database
12 changed files with 136 additions and 103 deletions

View File

@ -7,6 +7,7 @@ import {
useAppSelector,
useDownload,
useLibrary,
useUserDetails,
} from "@renderer/hooks";
import * as styles from "./app.css";
@ -19,7 +20,6 @@ import {
toggleDraggingDisabled,
closeToast,
} from "@renderer/features";
import { useUserAuth } from "./hooks/use-user-auth";
export interface AppProps {
children: React.ReactNode;
@ -31,7 +31,7 @@ export function App() {
const { clearDownload, setLastPacket } = useDownload();
const { updateUserAuth, clearUserAuth } = useUserAuth();
const { updateUser, clearUser } = useUserDetails();
const dispatch = useAppDispatch();
@ -39,9 +39,11 @@ export function App() {
const location = useLocation();
const search = useAppSelector((state) => state.search.value);
const draggingDisabled = useAppSelector(
(state) => state.window.draggingDisabled
);
const toast = useAppSelector((state) => state.toast);
useEffect(() => {
@ -70,20 +72,24 @@ export function App() {
};
}, [clearDownload, setLastPacket, updateLibrary]);
useEffect(() => {
updateUser();
}, [updateUser]);
useEffect(() => {
const listeners = [
window.electron.onSignIn(() => {
updateUserAuth();
updateUser();
}),
window.electron.onSignOut(() => {
clearUserAuth();
clearUser();
}),
];
return () => {
listeners.forEach((unsubscribe) => unsubscribe());
};
}, [clearUserAuth, updateUserAuth]);
}, [updateUser, clearUser]);
const handleSearch = useCallback(
(query: string) => {

View File

@ -1,24 +1,28 @@
import { useNavigate } from "react-router-dom";
import { PersonIcon } from "@primer/octicons-react";
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() {
const navigate = useNavigate();
const { userAuth, isLoading } = useUserAuth();
const { userDetails, profileBackground } = useUserDetails();
const handleClickProfile = () => {
navigate(`/user/${userAuth!.id}`);
navigate(`/user/${userDetails!.id}`);
};
const handleClickLogin = () => {
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 (
<>
<button
@ -43,14 +47,15 @@ export function SidebarProfile() {
<button
type="button"
className={styles.profileButton}
style={{ background: profileButtonBackground }}
onClick={handleClickProfile}
>
<div className={styles.profileAvatar}>
{userAuth.profileImageUrl ? (
{userDetails.profileImageUrl ? (
<img
className={styles.profileAvatar}
src={userAuth.profileImageUrl}
alt={userAuth.displayName}
src={userDetails.profileImageUrl}
alt={userDetails.displayName}
/>
) : (
<PersonIcon />
@ -58,7 +63,7 @@ export function SidebarProfile() {
</div>
<div className={styles.profileButtonInformation}>
<p style={{ fontWeight: "bold" }}>{userAuth.displayName}</p>
<p style={{ fontWeight: "bold" }}>{userDetails.displayName}</p>
</div>
</button>
</>

View File

@ -149,7 +149,9 @@ export const profileAvatar = style({
justifyContent: "center",
alignItems: "center",
backgroundColor: vars.color.background,
border: `solid 1px ${vars.color.border}`,
position: "relative",
objectFit: "cover",
});
export const profileButtonInformation = style({

View File

@ -4,4 +4,4 @@ export * from "./use-preferences-slice";
export * from "./download-slice";
export * from "./window-slice";
export * from "./toast-slice";
export * from "./user-auth-slice";
export * from "./user-details-slice";

View File

@ -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;

View 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;

View File

@ -3,3 +3,4 @@ export * from "./use-library";
export * from "./use-date";
export * from "./use-toast";
export * from "./redux";
export * from "./use-user-details";

View File

@ -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 };
}

View 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,
};
}

View File

@ -1,32 +1,27 @@
import { UserGame, UserProfile } from "@types";
import cn from "classnames";
import { average } from "color.js";
import * as styles from "./user.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 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 { buildGameDetailsPath, darkenColor } from "@renderer/helpers";
import { buildGameDetailsPath } from "@renderer/helpers";
import { PersonIcon } from "@primer/octicons-react";
import { Button } from "@renderer/components";
import { useUserAuth } from "@renderer/hooks/use-user-auth";
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
export interface ProfileContentProps {
userProfile: UserProfile;
}
export const UserContent = ({ userProfile }: ProfileContentProps) => {
export function UserContent({ userProfile }: ProfileContentProps) {
const { t, i18n } = useTranslation("user_profile");
const { userAuth, signOut } = useUserAuth();
const profileImageRef = useRef<HTMLImageElement | null>(null);
const [backgroundColors, setBackgroundColors] = useState<string[]>([]);
const { userDetails, profileBackground, signOut } = useUserDetails();
const navigate = useNavigate();
@ -64,34 +59,28 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => {
navigate("/");
};
const handleAvatarLoad = async () => {
const output = await average(profileImageRef.current!, {
amount: 1,
format: "hex",
});
const isMe = userDetails?.id == userProfile.id;
setBackgroundColors([
darkenColor(output as string, 0.6),
darkenColor(output as string, 0.7),
]);
};
const profileContentBoxBackground = useMemo(() => {
if (profileBackground) return profileBackground;
/* TODO: Render background colors for other users */
return undefined;
}, [profileBackground]);
return (
<>
<section
className={styles.profileContentBox}
style={{
backgroundImage: `linear-gradient(135deg, ${backgroundColors[0]}, ${backgroundColors[1]})`,
background: profileContentBoxBackground,
}}
>
<div className={styles.profileAvatarContainer}>
{userProfile.profileImageUrl ? (
<img
ref={profileImageRef}
className={styles.profileAvatar}
alt={userProfile.displayName}
src={userProfile.profileImageUrl}
onLoad={handleAvatarLoad}
/>
) : (
<PersonIcon size={72} />
@ -102,7 +91,7 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => {
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
</div>
{userAuth && userAuth.id == userProfile.id && (
{isMe && (
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
<Button theme="danger" onClick={handleSignout}>
{t("sign_out")}
@ -213,4 +202,4 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => {
</div>
</>
);
};
}

View File

@ -6,7 +6,7 @@ import {
searchSlice,
userPreferencesSlice,
toastSlice,
userAuthSlice,
userDetailsSlice,
} from "@renderer/features";
export const store = configureStore({
@ -17,7 +17,7 @@ export const store = configureStore({
userPreferences: userPreferencesSlice.reducer,
download: downloadSlice.reducer,
toast: toastSlice.reducer,
userAuth: userAuthSlice.reducer,
userDetails: userDetailsSlice.reducer,
},
});

View File

@ -244,7 +244,7 @@ export interface RealDebridUser {
expiration: string;
}
export interface UserAuth {
export interface UserDetails {
id: string;
displayName: string;
profileImageUrl: string | null;