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,
|
||||
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) => {
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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({
|
||||
|
@ -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";
|
||||
|
@ -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-toast";
|
||||
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 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -244,7 +244,7 @@ export interface RealDebridUser {
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
export interface UserAuth {
|
||||
export interface UserDetails {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
|
Loading…
Reference in New Issue
Block a user