diff --git a/src/main/events/cloud-save/check-game-cloud-sync-support.ts b/src/main/events/cloud-save/check-game-cloud-sync-support.ts deleted file mode 100644 index 4054d430..00000000 --- a/src/main/events/cloud-save/check-game-cloud-sync-support.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { registerEvent } from "../register-event"; -import { GameShop } from "@types"; -import { Ludusavi } from "@main/services"; - -const checkGameCloudSyncSupport = async ( - _event: Electron.IpcMainInvokeEvent, - objectId: string, - shop: GameShop -) => { - const games = await Ludusavi.findGames(shop, objectId); - return games.length === 1; -}; - -registerEvent("checkGameCloudSyncSupport", checkGameCloudSyncSupport); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 37e28447..1b621a30 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -61,7 +61,6 @@ import "./cloud-save/download-game-artifact"; import "./cloud-save/get-game-artifacts"; import "./cloud-save/get-game-backup-preview"; import "./cloud-save/upload-save-game"; -import "./cloud-save/check-game-cloud-sync-support"; import "./cloud-save/delete-game-artifact"; import "./notifications/publish-new-repacks-notification"; import { isPortableVersion } from "@main/helpers"; diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index eb80bc47..7b90e483 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -1,56 +1,75 @@ import { registerEvent } from "../register-event"; -import { HydraApi, PythonInstance } from "@main/services"; +import { HydraApi } from "@main/services"; import fs from "node:fs"; import path from "node:path"; import type { UpdateProfileRequest, UserProfile } from "@types"; import { omit } from "lodash-es"; import axios from "axios"; - -interface PresignedResponse { - presignedUrl: string; - profileImageUrl: string; -} +import { fileTypeFromFile } from "file-type"; const patchUserProfile = async (updateProfile: UpdateProfileRequest) => { return HydraApi.patch("/profile", updateProfile); }; -const getNewProfileImageUrl = async (localImageUrl: string) => { - const { imagePath, mimeType } = - await PythonInstance.processProfileImage(localImageUrl); - - const stats = fs.statSync(imagePath); +const uploadImage = async ( + type: "profile-image" | "background-image", + imagePath: string +) => { + const stat = fs.statSync(imagePath); const fileBuffer = fs.readFileSync(imagePath); - const fileSizeInBytes = stats.size; + const fileSizeInBytes = stat.size; - const { presignedUrl, profileImageUrl } = - await HydraApi.post(`/presigned-urls/profile-image`, { + const response = await HydraApi.post<{ presignedUrl: string }>( + `/presigned-urls/${type}`, + { imageExt: path.extname(imagePath).slice(1), imageLength: fileSizeInBytes, - }); + } + ); - await axios.put(presignedUrl, fileBuffer, { + const mimeType = await fileTypeFromFile(imagePath); + + await axios.put(response.presignedUrl, fileBuffer, { headers: { - "Content-Type": mimeType, + "Content-Type": mimeType?.mime, }, }); - return profileImageUrl; + if (type === "background-image") { + return response["backgroundImageUrl"]; + } + + return response["profileImageUrl"]; }; const updateProfile = async ( _event: Electron.IpcMainInvokeEvent, updateProfile: UpdateProfileRequest ) => { - if (!updateProfile.profileImageUrl) { - return patchUserProfile(omit(updateProfile, "profileImageUrl")); + const payload = omit(updateProfile, [ + "profileImageUrl", + "backgroundImageUrl", + ]); + + if (updateProfile.profileImageUrl) { + const profileImageUrl = await uploadImage( + "profile-image", + updateProfile.profileImageUrl + ).catch(() => undefined); + + payload["profileImageUrl"] = profileImageUrl; } - const profileImageUrl = await getNewProfileImageUrl( - updateProfile.profileImageUrl - ).catch(() => undefined); + if (updateProfile.backgroundImageUrl) { + const backgroundImageUrl = await uploadImage( + "background-image", + updateProfile.backgroundImageUrl + ).catch(() => undefined); - return patchUserProfile({ ...updateProfile, profileImageUrl }); + payload["backgroundImageUrl"] = backgroundImageUrl; + } + + return patchUserProfile(payload); }; registerEvent("updateProfile", updateProfile); diff --git a/src/preload/index.ts b/src/preload/index.ts index 40a6c101..11086199 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -160,8 +160,6 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("getGameArtifacts", objectId, shop), getGameBackupPreview: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameBackupPreview", objectId, shop), - checkGameCloudSyncSupport: (objectId: string, shop: GameShop) => - ipcRenderer.invoke("checkGameCloudSyncSupport", objectId, shop), deleteGameArtifact: (gameArtifactId: string) => ipcRenderer.invoke("deleteGameArtifact", gameArtifactId), onUploadComplete: (objectId: string, shop: GameShop, cb: () => void) => { diff --git a/src/renderer/src/components/avatar/avatar.css.ts b/src/renderer/src/components/avatar/avatar.css.ts new file mode 100644 index 00000000..34249860 --- /dev/null +++ b/src/renderer/src/components/avatar/avatar.css.ts @@ -0,0 +1,23 @@ +import { style } from "@vanilla-extract/css"; + +import { vars } from "../../theme.css"; + +export const profileAvatar = style({ + borderRadius: "4px", + display: "flex", + justifyContent: "center", + alignItems: "center", + backgroundColor: vars.color.background, + border: `solid 1px ${vars.color.border}`, + cursor: "pointer", + color: vars.color.muted, + position: "relative", +}); + +export const profileAvatarImage = style({ + height: "100%", + width: "100%", + objectFit: "cover", + overflow: "hidden", + borderRadius: "4px", +}); diff --git a/src/renderer/src/components/avatar/avatar.tsx b/src/renderer/src/components/avatar/avatar.tsx new file mode 100644 index 00000000..1a355872 --- /dev/null +++ b/src/renderer/src/components/avatar/avatar.tsx @@ -0,0 +1,32 @@ +import { PersonIcon } from "@primer/octicons-react"; + +import * as styles from "./avatar.css"; + +export interface AvatarProps + extends Omit< + React.DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement + >, + "src" + > { + size: number; + src?: string | null; +} + +export function Avatar({ size, alt, src, ...props }: AvatarProps) { + return ( +
+ {src ? ( + {alt} + ) : ( + + )} +
+ ); +} diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index c548be52..869cb2d6 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -60,7 +60,12 @@ export function GameCard({ game, ...props }: GameCardProps) { onMouseEnter={handleHover} >
- {game.title} + {game.title}
diff --git a/src/renderer/src/components/hero/hero.tsx b/src/renderer/src/components/hero/hero.tsx index 0e5a7849..9986a7d8 100644 --- a/src/renderer/src/components/hero/hero.tsx +++ b/src/renderer/src/components/hero/hero.tsx @@ -49,7 +49,12 @@ export function Hero() {
{game.logo && ( - {game.description} + {game.description} )}

{game.description}

diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index 52124238..65d07440 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -1,3 +1,4 @@ +export * from "./avatar/avatar"; export * from "./bottom-panel/bottom-panel"; export * from "./button/button"; export * from "./game-card/game-card"; @@ -12,3 +13,4 @@ export * from "./select-field/select-field"; export * from "./toast/toast"; export * from "./badge/badge"; export * from "./confirmation-modal/confirmation-modal"; +export * from "./suspense-wrapper/suspense-wrapper"; diff --git a/src/renderer/src/components/sidebar/sidebar-profile.css.ts b/src/renderer/src/components/sidebar/sidebar-profile.css.ts index f8e0e969..bed9ac93 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.css.ts +++ b/src/renderer/src/components/sidebar/sidebar-profile.css.ts @@ -31,19 +31,6 @@ export const profileButtonContent = style({ width: "100%", }); -export const profileAvatar = style({ - width: "35px", - height: "35px", - borderRadius: "4px", - display: "flex", - justifyContent: "center", - alignItems: "center", - backgroundColor: vars.color.background, - border: `solid 1px ${vars.color.border}`, - position: "relative", - objectFit: "cover", -}); - export const profileButtonInformation = style({ display: "flex", flexDirection: "column", diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index b241ef0e..79acf414 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -1,11 +1,12 @@ import { useNavigate } from "react-router-dom"; -import { PeopleIcon, PersonIcon } from "@primer/octicons-react"; +import { PeopleIcon } from "@primer/octicons-react"; import * as styles from "./sidebar-profile.css"; import { useAppSelector, useUserDetails } from "@renderer/hooks"; import { useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; +import { Avatar } from "../avatar/avatar"; const LONG_POLLING_INTERVAL = 60_000; @@ -94,17 +95,11 @@ export function SidebarProfile() { onClick={handleProfileClick} >
-
- {userDetails?.profileImageUrl ? ( - {userDetails.displayName} - ) : ( - - )} -
+

diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index c509e489..206fdc81 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -225,6 +225,7 @@ export function Sidebar() { className={styles.gameIcon} src={game.iconUrl} alt={game.title} + loading="lazy" /> ) : ( diff --git a/src/renderer/src/components/suspense-wrapper/suspense-wrapper.tsx b/src/renderer/src/components/suspense-wrapper/suspense-wrapper.tsx new file mode 100644 index 00000000..d5888d33 --- /dev/null +++ b/src/renderer/src/components/suspense-wrapper/suspense-wrapper.tsx @@ -0,0 +1,13 @@ +import { Suspense } from "react"; + +export interface SuspenseWrapperProps { + Component: React.LazyExoticComponent<() => JSX.Element>; +} + +export function SuspenseWrapper({ Component }: SuspenseWrapperProps) { + return ( + + + + ); +} diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index 086a8c94..5a0a66f0 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -23,13 +23,13 @@ export interface CloudSyncContext { artifacts: GameArtifact[]; showCloudSyncModal: boolean; showCloudSyncFilesModal: boolean; - supportsCloudSync: boolean | null; backupState: CloudSyncState; setShowCloudSyncModal: React.Dispatch>; downloadGameArtifact: (gameArtifactId: string) => Promise; uploadSaveGame: () => Promise; deleteGameArtifact: (gameArtifactId: string) => Promise; setShowCloudSyncFilesModal: React.Dispatch>; + getGameBackupPreview: () => Promise; restoringBackup: boolean; uploadingBackup: boolean; } @@ -37,7 +37,6 @@ export interface CloudSyncContext { export const cloudSyncContext = createContext({ backupPreview: null, showCloudSyncModal: false, - supportsCloudSync: null, backupState: CloudSyncState.Unknown, setShowCloudSyncModal: () => {}, downloadGameArtifact: async () => {}, @@ -46,6 +45,7 @@ export const cloudSyncContext = createContext({ deleteGameArtifact: async () => {}, showCloudSyncFilesModal: false, setShowCloudSyncFilesModal: () => {}, + getGameBackupPreview: async () => {}, restoringBackup: false, uploadingBackup: false, }); @@ -66,9 +66,6 @@ export function CloudSyncContextProvider({ }: CloudSyncContextProviderProps) { const { t } = useTranslation("game_details"); - const [supportsCloudSync, setSupportsCloudSync] = useState( - null - ); const [artifacts, setArtifacts] = useState([]); const [showCloudSyncModal, setShowCloudSyncModal] = useState(false); const [backupPreview, setBackupPreview] = useState( @@ -89,21 +86,26 @@ export function CloudSyncContextProvider({ ); const getGameBackupPreview = useCallback(async () => { - window.electron.getGameArtifacts(objectId, shop).then((results) => { - setArtifacts(results); - }); - - window.electron - .getGameBackupPreview(objectId, shop) - .then((preview) => { - logger.info("Game backup preview", objectId, shop, preview); - if (preview && Object.keys(preview.games).length) { - setBackupPreview(preview); - } - }) - .catch((err) => { - logger.error("Failed to get game backup preview", objectId, shop, err); - }); + await Promise.allSettled([ + window.electron.getGameArtifacts(objectId, shop).then((results) => { + setArtifacts(results); + }), + window.electron + .getGameBackupPreview(objectId, shop) + .then((preview) => { + if (preview && Object.keys(preview.games).length) { + setBackupPreview(preview); + } + }) + .catch((err) => { + logger.error( + "Failed to get game backup preview", + objectId, + shop, + err + ); + }), + ]); }, [objectId, shop]); const uploadSaveGame = useCallback(async () => { @@ -152,33 +154,14 @@ export function CloudSyncContextProvider({ [getGameBackupPreview] ); - useEffect(() => { - window.electron - .checkGameCloudSyncSupport(objectId, shop) - .then((result) => { - logger.info("Cloud sync support", objectId, shop, result); - setSupportsCloudSync(result); - }) - .catch((err) => { - logger.error("Failed to check cloud sync support", err); - }); - }, [objectId, shop, getGameBackupPreview]); - useEffect(() => { setBackupPreview(null); setArtifacts([]); - setSupportsCloudSync(null); setShowCloudSyncModal(false); setRestoringBackup(false); setUploadingBackup(false); }, [objectId, shop]); - useEffect(() => { - if (showCloudSyncModal) { - getGameBackupPreview(); - } - }, [getGameBackupPreview, showCloudSyncModal]); - const backupState = useMemo(() => { if (!backupPreview) return CloudSyncState.Unknown; if (backupPreview.overall.changedGames.new) return CloudSyncState.New; @@ -192,7 +175,6 @@ export function CloudSyncContextProvider({ return ( {children} diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx index 1eb0c47c..98a25a77 100644 --- a/src/renderer/src/context/user-profile/user-profile.context.tsx +++ b/src/renderer/src/context/user-profile/user-profile.context.tsx @@ -13,8 +13,9 @@ export interface UserProfileContext { /* Indicates if the current user is viewing their own profile */ isMe: boolean; userStats: UserStats | null; - getUserProfile: () => Promise; + setSelectedBackgroundImage: React.Dispatch>; + backgroundImage: string; } export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3"; @@ -25,6 +26,8 @@ export const userProfileContext = createContext({ isMe: false, userStats: null, getUserProfile: async () => {}, + setSelectedBackgroundImage: () => {}, + backgroundImage: "", }); const { Provider } = userProfileContext; @@ -47,6 +50,9 @@ export function UserProfileContextProvider({ const [heroBackground, setHeroBackground] = useState( DEFAULT_USER_PROFILE_BACKGROUND ); + const [selectedBackgroundImage, setSelectedBackgroundImage] = useState(""); + + const isMe = userDetails?.id === userProfile?.id; const getHeroBackgroundFromImageUrl = async (imageUrl: string) => { const output = await average(imageUrl, { @@ -57,6 +63,14 @@ export function UserProfileContextProvider({ return `linear-gradient(135deg, ${darkenColor(output as string, 0.5)}, ${darkenColor(output as string, 0.6, 0.5)})`; }; + const getBackgroundImageUrl = () => { + if (selectedBackgroundImage && isMe) + return `local:${selectedBackgroundImage}`; + if (userProfile?.backgroundImageUrl) return userProfile.backgroundImageUrl; + + return ""; + }; + const { t } = useTranslation("user_profile"); const { showErrorToast } = useToast(); @@ -99,8 +113,10 @@ export function UserProfileContextProvider({ value={{ userProfile, heroBackground, - isMe: userDetails?.id === userProfile?.id, + isMe, getUserProfile, + setSelectedBackgroundImage, + backgroundImage: getBackgroundImageUrl(), userStats, }} > diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 677c3ee2..c9d4e5e8 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -138,10 +138,6 @@ declare global { objectId: string, shop: GameShop ) => Promise; - checkGameCloudSyncSupport: ( - objectId: string, - shop: GameShop - ) => Promise; deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>; onBackupDownloadComplete: ( objectId: string, diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 5f91b424..bc3522ae 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -15,15 +15,6 @@ import "@fontsource/noto-sans/700.css"; import "react-loading-skeleton/dist/skeleton.css"; import { App } from "./app"; -import { - Home, - Downloads, - GameDetails, - SearchResults, - Settings, - Catalogue, - Profile, -} from "@renderer/pages"; import { store } from "./store"; @@ -33,6 +24,17 @@ import { AchievementNotification } from "./pages/achievement/notification/achiev import "./workers"; import { RepacksContextProvider } from "./context"; import { Achievement } from "./pages/achievement/achievements"; +import { SuspenseWrapper } from "./components"; + +const Home = React.lazy(() => import("./pages/home/home")); +const GameDetails = React.lazy( + () => import("./pages/game-details/game-details") +); +const Downloads = React.lazy(() => import("./pages/downloads/downloads")); +const SearchResults = React.lazy(() => import("./pages/home/search-results")); +const Settings = React.lazy(() => import("./pages/settings/settings")); +const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue")); +const Profile = React.lazy(() => import("./pages/profile/profile")); Sentry.init({}); @@ -63,13 +65,31 @@ ReactDOM.createRoot(document.getElementById("root")!).render( }> - - - - - - - + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> { + const aboutTheGame = shopDetails?.about_the_game; + if (aboutTheGame) { + const document = new DOMParser().parseFromString( + aboutTheGame, + "text/html" + ); + + const $images = Array.from(document.querySelectorAll("img")); + $images.forEach(($image) => { + $image.loading = "lazy"; + }); + + return document.body.outerHTML; + } + + return t("no_shop_details"); + }, [shopDetails, t]); + const [backdropOpactiy, setBackdropOpacity] = useState(1); const handleHeroLoad = async () => { @@ -87,6 +106,10 @@ export function GameDetailsContent() { setShowCloudSyncModal(true); }; + useEffect(() => { + getGameBackupPreview(); + }, [getGameBackupPreview]); + return (

{game?.title} - {supportsCloudSync && ( - - )} + +
+ {t("cloud_save")} +
@@ -160,7 +181,7 @@ export function GameDetailsContent() {
diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index f2b928b5..bab9452f 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -29,7 +29,7 @@ import { Downloader, getDownloadersForUri } from "@shared"; import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal"; import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal"; -export function GameDetails() { +export default function GameDetails() { const [randomGame, setRandomGame] = useState(null); const [randomizerLocked, setRandomizerLocked] = useState(false); diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index e70a58fd..ad306726 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -16,7 +16,7 @@ import Lottie, { type LottieRefCurrentProps } from "lottie-react"; import { buildGameDetailsPath } from "@renderer/helpers"; import { CatalogueCategory } from "@shared"; -export function Home() { +export default function Home() { const { t } = useTranslation("home"); const navigate = useNavigate(); diff --git a/src/renderer/src/pages/home/search-results.tsx b/src/renderer/src/pages/home/search-results.tsx index 32c4ad89..d86a362a 100644 --- a/src/renderer/src/pages/home/search-results.tsx +++ b/src/renderer/src/pages/home/search-results.tsx @@ -17,7 +17,7 @@ import { buildGameDetailsPath } from "@renderer/helpers"; import { vars } from "@renderer/theme.css"; -export function SearchResults() { +export default function SearchResults() { const dispatch = useAppDispatch(); const { t } = useTranslation("home"); diff --git a/src/renderer/src/pages/index.ts b/src/renderer/src/pages/index.ts deleted file mode 100644 index 5dc22c2d..00000000 --- a/src/renderer/src/pages/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./home/home"; -export * from "./game-details/game-details"; -export * from "./downloads/downloads"; -export * from "./home/search-results"; -export * from "./settings/settings"; -export * from "./catalogue/catalogue"; -export * from "./profile/profile"; diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts index bd873a8a..b4232096 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts @@ -3,28 +3,18 @@ import { globalStyle, style } from "@vanilla-extract/css"; export const profileAvatarEditContainer = style({ alignSelf: "center", - width: "128px", - height: "128px", + // width: "132px", + // height: "132px", display: "flex", - borderRadius: "4px", + // borderRadius: "4px", color: vars.color.body, justifyContent: "center", alignItems: "center", backgroundColor: vars.color.background, position: "relative", - border: `solid 1px ${vars.color.border}`, - boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", cursor: "pointer", }); -export const profileAvatar = style({ - height: "100%", - width: "100%", - objectFit: "cover", - borderRadius: "4px", - overflow: "hidden", -}); - export const profileAvatarEditOverlay = style({ position: "absolute", width: "100%", diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx index cd43641a..cad4ed9e 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx @@ -2,8 +2,9 @@ import { useContext, useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { Trans, useTranslation } from "react-i18next"; -import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react"; +import { DeviceCameraIcon } from "@primer/octicons-react"; import { + Avatar, Button, Link, Modal, @@ -111,14 +112,14 @@ export function EditProfileModal( if (filePaths && filePaths.length > 0) { const path = filePaths[0]; - const { imagePath } = await window.electron - .processProfileImage(path) - .catch(() => { - showErrorToast(t("image_process_failure")); - return { imagePath: null }; - }); + // const { imagePath } = await window.electron + // .processProfileImage(path) + // .catch(() => { + // showErrorToast(t("image_process_failure")); + // return { imagePath: null }; + // }); - onChange(imagePath); + onChange(path); } }; @@ -138,15 +139,11 @@ export function EditProfileModal( className={styles.profileAvatarEditContainer} onClick={handleChangeProfileAvatar} > - {imageUrl ? ( - {userDetails?.displayName} - ) : ( - - )} +
diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.tsx b/src/renderer/src/pages/profile/profile-content/friends-box.tsx index 151f9c80..82d4ff9d 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/friends-box.tsx @@ -4,8 +4,7 @@ import { useContext } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./profile-content.css"; -import { Link } from "@renderer/components"; -import { PersonIcon } from "@primer/octicons-react"; +import { Avatar, Link } from "@renderer/components"; export function FriendsBox() { const { userProfile, userStats } = useContext(userProfileContext); @@ -30,17 +29,11 @@ export function FriendsBox() { {userProfile?.friends.map((friend) => (
  • - {friend.profileImageUrl ? ( - {friend.displayName} - ) : ( -
    - -
    - )} + {friend.displayName} diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts index 46e32556..cdebc5df 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts @@ -1,17 +1,5 @@ import { SPACING_UNIT, vars } from "../../../theme.css"; -import { keyframes, style } from "@vanilla-extract/css"; - -const animateBackground = keyframes({ - "0%": { - backgroundPosition: "0% 50%", - }, - "50%": { - backgroundPosition: "100% 50%", - }, - "100%": { - backgroundPosition: "0% 50%", - }, -}); +import { style } from "@vanilla-extract/css"; export const profileContentBox = style({ display: "flex", @@ -74,7 +62,7 @@ export const heroPanel = style({ display: "flex", gap: `${SPACING_UNIT}px`, justifyContent: "space-between", - backdropFilter: `blur(10px)`, + backdropFilter: `blur(15px)`, borderTop: `solid 1px rgba(255, 255, 255, 0.1)`, boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)", backgroundColor: "rgba(0, 0, 0, 0.3)", @@ -99,25 +87,3 @@ export const currentGameDetails = style({ gap: `${SPACING_UNIT}px`, alignItems: "center", }); - -export const xdTotal = style({ - background: `linear-gradient( - 60deg, - #f79533, - #f37055, - #ef4e7b, - #a166ab, - #5073b8, - #1098ad, - #07b39b, - #6fba82 - )`, - width: "102px", - minWidth: "102px", - height: "102px", - animation: `${animateBackground} 4s ease alternate infinite`, - backgroundSize: "300% 300%", - zIndex: -1, - borderRadius: "4px", - position: "absolute", -}); diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index 8b70945c..b8528810 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -8,13 +8,11 @@ import { CheckCircleFillIcon, PencilIcon, PersonAddIcon, - PersonIcon, SignOutIcon, - UploadIcon, XCircleFillIcon, } from "@primer/octicons-react"; import { buildGameDetailsPath } from "@renderer/helpers"; -import { Button, Link } from "@renderer/components"; +import { Avatar, Button, Link } from "@renderer/components"; import { useTranslation } from "react-i18next"; import { useAppSelector, @@ -28,16 +26,21 @@ import { useNavigate } from "react-router-dom"; import type { FriendRequestAction } from "@types"; import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal"; import Skeleton from "react-loading-skeleton"; +import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button"; type FriendAction = | FriendRequestAction | ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND"); +const backgroundImageLayer = + "linear-gradient(135deg, rgb(0 0 0 / 50%), rgb(0 0 0 / 60%))"; + export function ProfileHero() { const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false); - const { isMe, getUserProfile, userProfile } = useContext(userProfileContext); + const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } = + useContext(userProfileContext); const { signOut, updateFriendRequestState, @@ -48,8 +51,6 @@ export function ProfileHero() { const { gameRunning } = useAppSelector((state) => state.gameRunning); - const [hero, setHero] = useState(""); - const { t } = useTranslation("user_profile"); const { formatDistance } = useDate(); @@ -186,6 +187,7 @@ export function ProfileHero() { handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP") } disabled={isPerformingAction} + style={{ borderColor: vars.color.body }} > {t("undo_friendship")} @@ -260,35 +262,6 @@ export function ProfileHero() { return userProfile?.currentGame; }, [isMe, userProfile, gameRunning]); - const handleChangeCoverClick = async () => { - const { filePaths } = await window.electron.showOpenDialog({ - properties: ["openFile"], - filters: [ - { - name: "Image", - extensions: ["jpg", "jpeg", "png", "gif", "webp"], - }, - ], - }); - - if (filePaths && filePaths.length > 0) { - const path = filePaths[0]; - - setHero(path); - - // onChange(imagePath); - } - }; - - const getImageUrl = () => { - if (hero) return `local:${hero}`; - // if (userDetails?.profileImageUrl) return userDetails.profileImageUrl; - - return ""; - }; - - // const imageUrl = getImageUrl(); - return ( <> {/* setShowEditProfileModal(false)} /> -
    - +
    + {backgroundImage && ( + + )} +
    -
    - {userProfile?.profileImageUrl ? ( - {userProfile?.displayName} - ) : ( - - )} +
    @@ -379,28 +352,14 @@ export function ProfileHero() { )}
    - +
    { + try { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: "Image", + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + const path = filePaths[0]; + + setSelectedBackgroundImage(path); + setIsUploadingBackgorundImage(true); + + await patchUser({ backgroundImageUrl: path }); + + showSuccessToast("Background image updated"); + } + } finally { + setIsUploadingBackgorundImage(false); + } + }; + + if (!isMe) return null; + + return ( + + ); +} diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index be7e9597..dffdfbae 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -15,7 +15,7 @@ import { SettingsPrivacy } from "./settings-privacy"; import { useUserDetails } from "@renderer/hooks"; import { useMemo } from "react"; -export function Settings() { +export default function Settings() { const { t } = useTranslation("settings"); const { userDetails } = useUserDetails(); diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx index 3ca837fa..38f0dd25 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx @@ -1,11 +1,8 @@ -import { - CheckCircleIcon, - PersonIcon, - XCircleIcon, -} from "@primer/octicons-react"; +import { CheckCircleIcon, XCircleIcon } from "@primer/octicons-react"; import * as styles from "./user-friend-modal.css"; import { SPACING_UNIT } from "@renderer/theme.css"; import { useTranslation } from "react-i18next"; +import { Avatar } from "@renderer/components"; export type UserFriendItemProps = { userId: string; @@ -109,17 +106,8 @@ export const UserFriendItem = (props: UserFriendItemProps) => { return (
    -
    - {profileImageUrl ? ( - {displayName} - ) : ( - - )} -
    + +
    { className={styles.friendListButton} onClick={() => props.onClickItem(userId)} > -
    - {profileImageUrl ? ( - {displayName} - ) : ( - - )} -
    +