diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91069c4b..05066c1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20.11.1 - cache: "yarn" - name: Install dependencies run: yarn @@ -27,7 +26,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.9 - cache: "pip" - name: Install dependencies run: pip install -r requirements.txt @@ -63,7 +61,6 @@ jobs: with: name: Build-${{ matrix.os }} path: | - dist/win-unpacked/** dist/*-portable.exe dist/*.zip dist/*.dmg diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 6e749b3e..a4426d2c 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -339,6 +339,7 @@ "achievement_unlocked": "Achievement unlocked", "user_achievements": "{{displayName}}'s Achievements", "your_achievements": "Your Achievements", - "unlocked_at": "Unlocked at:" + "unlocked_at": "Unlocked at:", + "subscription_needed": "A Hydra Cloud subscription is needed to see this content" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index ba572424..77ebe906 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -341,6 +341,7 @@ "achievement_unlocked": "Conquista desbloqueada", "your_achievements": "Suas Conquistas", "user_achievements": "Conquistas de {{displayName}}", - "unlocked_at": "Desbloqueado em:" + "unlocked_at": "Desbloqueado em:", + "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo" } } diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index b59db09f..dac181ee 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -283,6 +283,7 @@ }, "achievement": { "achievement_unlocked": "Conquista desbloqueada", - "unlocked_at": "Desbloqueado em:" + "unlocked_at": "Desbloqueado em:", + "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo" } } diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts index 93e89441..734366c8 100644 --- a/src/main/events/catalogue/get-game-achievements.ts +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -51,7 +51,7 @@ const getAchievementLocalUser = async (shop: string, objectId: string) => { ...achievementData, unlocked: false, unlockTime: null, - icon: icongray, + icongray: icongray, } as UserAchievement; }) .sort((a, b) => { @@ -110,7 +110,7 @@ const getAchievementsRemoteUser = async ( ...achievementData, unlocked: false, unlockTime: null, - icon: icongray, + icongray: icongray, } as UserAchievement; }) .sort((a, b) => { diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 80363e4e..ffdfc354 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -50,6 +50,7 @@ import "./user/unblock-user"; import "./user/get-user-friends"; import "./user/get-user-stats"; import "./user/report-user"; +import "./user/get-compared-unlocked-achievements"; import "./profile/get-friend-requests"; import "./profile/get-me"; import "./profile/undo-friendship"; diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts new file mode 100644 index 00000000..8c5c8779 --- /dev/null +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -0,0 +1,44 @@ +import type { ComparedAchievements, GameShop } from "@types"; +import { registerEvent } from "../register-event"; +import { userPreferencesRepository } from "@main/repository"; +import { HydraApi } from "@main/services"; + +const getComparedUnlockedAchievements = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop, + userId: string +) => { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + return HydraApi.get( + `/users/${userId}/games/achievements/compare`, + { + shop, + objectId, + language: userPreferences?.language || "en", + } + ).then((achievements) => { + const sortedAchievements = achievements.achievements.sort((a, b) => { + if (a.otherUserStat.unlocked && !b.otherUserStat.unlocked) return -1; + if (!a.otherUserStat.unlocked && b.otherUserStat.unlocked) return 1; + if (a.otherUserStat.unlocked && b.otherUserStat.unlocked) { + return b.otherUserStat.unlockTime! - a.otherUserStat.unlockTime!; + } + + return Number(a.hidden) - Number(b.hidden); + }); + + return { + ...achievements, + achievements: sortedAchievements, + } as ComparedAchievements; + }); +}; + +registerEvent( + "getComparedUnlockedAchievements", + getComparedUnlockedAchievements +); diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index ac078468..51f1351e 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -9,7 +9,7 @@ import { getAlternativeObjectIds, } from "./find-achivement-files"; import type { AchievementFile } from "@types"; -import { achievementsLogger, logger } from "../logger"; +import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; const fileStats: Map = new Map(); @@ -55,8 +55,6 @@ const processAchievementFileDiff = async ( ) => { const unlockedAchievements = parseAchievementFile(file.filePath, file.type); - logger.log("Achievements from file", file.filePath, unlockedAchievements); - if (unlockedAchievements.length) { return mergeAchievements( game.objectID, @@ -80,7 +78,7 @@ const compareFltFolder = async (game: Game, file: AchievementFile) => { return; } - logger.log("Detected change in FLT folder", file.filePath); + achievementsLogger.log("Detected change in FLT folder", file.filePath); await processAchievementFileDiff(game, file); } catch (err) { achievementsLogger.error(err); @@ -101,6 +99,13 @@ const compareFile = async (game: Game, file: AchievementFile) => { if (!previousStat) { if (currentStat.mtimeMs) { + achievementsLogger.log( + "First change in file", + file.filePath, + previousStat, + currentStat.mtimeMs + ); + await processAchievementFileDiff(game, file); return; } @@ -110,7 +115,7 @@ const compareFile = async (game: Game, file: AchievementFile) => { return; } - logger.log( + achievementsLogger.log( "Detected change in file", file.filePath, previousStat, diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index 443af59a..552e66b7 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -42,7 +42,9 @@ export const getGameAchievementData = async ( where: { objectId, shop }, }) .then((gameAchievements) => { - return JSON.parse(gameAchievements?.achievements || "[]"); + return JSON.parse( + gameAchievements?.achievements || "[]" + ) as AchievementData[]; }); }); }; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index c97e8f56..367a5550 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -64,7 +64,13 @@ export const mergeAchievements = async ( localGameAchievement?.unlockedAchievements || "[]" ).filter((achievement) => achievement.name) as UnlockedAchievement[]; - const newAchievements = achievements + const newAchievementsMap = new Map( + achievements.reverse().map((achievement) => { + return [achievement.name.toUpperCase(), achievement]; + }) + ); + + const newAchievements = [...newAchievementsMap.values()] .filter((achievement) => { return !unlockedAchievements.some((localAchievement) => { return ( @@ -114,7 +120,7 @@ export const mergeAchievements = async ( const mergedLocalAchievements = unlockedAchievements.concat(newAchievements); - if (game?.remoteId) { + if (game.remoteId) { return HydraApi.put( "/profile/games/achievements", { diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index 9c875a3f..eb6321a3 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -242,15 +242,23 @@ const processRld = (unlockedAchievements: any): UnlockedAchievement[] => { const unlockedAchievement = unlockedAchievements[achievement]; if (unlockedAchievement?.State) { - newUnlockedAchievements.push({ - name: achievement, - unlockTime: - new DataView( - new Uint8Array( - Buffer.from(unlockedAchievement.Time.toString(), "hex") - ).buffer - ).getUint32(0, true) * 1000, - }); + const unlocked = new DataView( + new Uint8Array( + Buffer.from(unlockedAchievement.State.toString(), "hex") + ).buffer + ).getUint32(0, true); + + if (unlocked === 1) { + newUnlockedAchievements.push({ + name: achievement, + unlockTime: + new DataView( + new Uint8Array( + Buffer.from(unlockedAchievement.Time.toString(), "hex") + ).buffer + ).getUint32(0, true) * 1000, + }); + } } } diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 13f33fcd..b67055ce 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -49,14 +49,14 @@ export const updateAllLocalUnlockedAchievements = async () => { if (parsedAchievements.length) { unlockedAchievements.push(...parsedAchievements); - } - achievementsLogger.log( - "Achievement file for", - game.title, - achievementFile.filePath, - parsedAchievements - ); + achievementsLogger.log( + "Achievement file for", + game.title, + achievementFile.filePath, + parsedAchievements + ); + } } mergeAchievements(game.objectID, "steam", unlockedAchievements, false); diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index eaa05f7a..ecdf64f5 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -80,6 +80,10 @@ export class WindowManager { this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders( (details, callback) => { + if (details.webContentsId !== this.mainWindow?.webContents.id) { + return callback(details); + } + const userAgent = new UserAgent(); callback({ diff --git a/src/preload/index.ts b/src/preload/index.ts index fc2e5a91..0eadd409 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -259,6 +259,17 @@ contextBridge.exposeInMainWorld("electron", { getUserStats: (userId: string) => ipcRenderer.invoke("getUserStats", userId), reportUser: (userId: string, reason: string, description: string) => ipcRenderer.invoke("reportUser", userId, reason, description), + getComparedUnlockedAchievements: ( + objectId: string, + shop: GameShop, + userId: string + ) => + ipcRenderer.invoke( + "getComparedUnlockedAchievements", + objectId, + shop, + userId + ), /* Auth */ signOut: () => ipcRenderer.invoke("signOut"), diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 91bed316..e6a47959 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -29,6 +29,7 @@ import type { GameArtifact, LudusaviBackup, UserAchievement, + ComparedAchievements, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type { DiskSpace } from "check-disk-space"; @@ -202,6 +203,11 @@ declare global { reason: string, description: string ) => Promise; + getComparedUnlockedAchievements: ( + objectId: string, + shop: GameShop, + userId: string + ) => Promise; /* Profile */ getMe: () => Promise; diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index d4563330..a241bf47 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -36,15 +36,13 @@ export const buildGameDetailsPath = ( export const buildGameAchievementPath = ( game: { shop: GameShop; objectId: string; title: string }, - user?: { userId: string; displayName: string; profileImageUrl: string | null } + user?: { userId: string } ) => { const searchParams = new URLSearchParams({ title: game.title, shop: game.shop, objectId: game.objectId, userId: user?.userId || "", - displayName: user?.displayName || "", - profileImageUrl: user?.profileImageUrl || "", }); return `/achievements/?${searchParams.toString()}`; diff --git a/src/renderer/src/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts index 21ed1b34..9486400c 100644 --- a/src/renderer/src/hooks/use-date.ts +++ b/src/renderer/src/hooks/use-date.ts @@ -72,7 +72,7 @@ export function useDate() { const locale = getDateLocale(); return format( date, - locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy - HH:mm" + locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy HH:mm" ); }, diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 649d24a4..1872c95d 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { useAppDispatch, useAppSelector } from "./redux"; import { setProfileBackground, @@ -129,7 +129,16 @@ export function useUserDetails() { const unblockUser = (userId: string) => window.electron.unblockUser(userId); - const hasActiveSubscription = userDetails?.subscription?.status === "active"; + const hasActiveSubscription = useMemo(() => { + if (!userDetails?.subscription) { + return false; + } + + return ( + userDetails.subscription.expiresAt == null || + new Date(userDetails.subscription.expiresAt) > new Date() + ); + }, [userDetails]); return { userDetails, diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index 6b2bd1e1..ca60be70 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -1,48 +1,57 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useDate, useUserDetails } from "@renderer/hooks"; import { steamUrlBuilder } from "@shared"; -import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; -import { formatDownloadProgress } from "@renderer/helpers"; -import { PersonIcon, TrophyIcon } from "@primer/octicons-react"; +import { + buildGameDetailsPath, + formatDownloadProgress, +} from "@renderer/helpers"; +import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; -import { UserAchievement } from "@types"; +import { ComparedAchievements, UserAchievement } from "@types"; import { average } from "color.js"; import Color from "color"; - -const HERO_ANIMATION_THRESHOLD = 25; +import { Link } from "@renderer/components"; +import { ComparedAchievementList } from "./compared-achievement-list"; interface UserInfo { userId: string; displayName: string; - achievements: UserAchievement[]; profileImageUrl: string | null; + totalAchievementCount: number; + unlockedAchievementCount: number; } interface AchievementsContentProps { otherUser: UserInfo | null; + comparedAchievements: ComparedAchievements | null; } interface AchievementListProps { achievements: UserAchievement[]; - otherUserAchievements?: UserAchievement[]; } -interface AchievementPanelProps { +interface AchievementSummaryProps { user: UserInfo; - otherUser: UserInfo | null; + isComparison?: boolean; } -function AchievementPanel({ user, otherUser }: AchievementPanelProps) { +function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { const { t } = useTranslation("achievement"); + const { userDetails, hasActiveSubscription } = useUserDetails(); - const getProfileImage = (imageUrl: string | null | undefined) => { + const getProfileImage = (user: UserInfo) => { return (
- {imageUrl ? ( - {"teste"} + {user.profileImageUrl ? ( + {user.displayName} ) : ( )} @@ -50,331 +59,160 @@ function AchievementPanel({ user, otherUser }: AchievementPanelProps) { ); }; - const userTotalAchievementCount = user.achievements.length; - const userUnlockedAchievementCount = user.achievements.filter( - (achievement) => achievement.unlocked - ).length; - - if (!otherUser) { + if ( + isComparison && + userDetails?.id == user.userId && + !hasActiveSubscription + ) { return (
- {getProfileImage(user.profileImageUrl)} +
+ +

+ {t("subscription_needed")} +

+
-

- {t("your_achievements")} -

-
-
- - - {userUnlockedAchievementCount} / {userTotalAchievementCount} - -
- - - {formatDownloadProgress( - userUnlockedAchievementCount / userTotalAchievementCount - )} - -
- + {getProfileImage(user)} +

{user.displayName}

); } - const otherUserUnlockedAchievementCount = otherUser.achievements.filter( - (achievement) => achievement.unlocked - ).length; - const otherUserTotalAchievementCount = otherUser.achievements.length; - return ( - <> +
+ {getProfileImage(user)}
- {getProfileImage(otherUser.profileImageUrl)} +

{user.displayName}

-

- {t("user_achievements", { - displayName: otherUser.displayName, - })} -

-
- - - {otherUserUnlockedAchievementCount} /{" "} - {otherUserTotalAchievementCount} - -
- + - {formatDownloadProgress( - otherUserUnlockedAchievementCount / - otherUserTotalAchievementCount - )} + {user.unlockedAchievementCount} / {user.totalAchievementCount}
- -
-
-
- {getProfileImage(user.profileImageUrl)} -
-

- {t("your_achievements")} -

-
-
- - - {userUnlockedAchievementCount} / {userTotalAchievementCount} - -
- - {formatDownloadProgress( - userUnlockedAchievementCount / userTotalAchievementCount - )} - -
- + + {formatDownloadProgress( + user.unlockedAchievementCount / user.totalAchievementCount + )} +
+
- +
); } -function AchievementList({ - achievements, - otherUserAchievements, -}: AchievementListProps) { +function AchievementList({ achievements }: AchievementListProps) { const { t } = useTranslation("achievement"); const { formatDateTime } = useDate(); - if (!otherUserAchievements || otherUserAchievements.length === 0) { - return ( -
    - {achievements.map((achievement, index) => ( -
  • - {achievement.displayName} -
    -

    {achievement.displayName}

    -

    {achievement.description}

    -
    - {achievement.unlockTime && ( -
    - {t("unlocked_at")} -

    {formatDateTime(achievement.unlockTime)}

    -
    - )} -
  • - ))} -
- ); - } - return (
    - {otherUserAchievements.map((otherUserAchievement, index) => ( -
  • -
    - {otherUserAchievement.displayName} - {otherUserAchievement.unlockTime && ( -
    - {t("unlocked_at")} -

    {formatDateTime(otherUserAchievement.unlockTime)}

    -
    - )} -
    - -
    -

    {otherUserAchievement.displayName}

    -

    {otherUserAchievement.description}

    -
    - -
    - {achievements[index].displayName} - {achievements[index].unlockTime && ( -
    - {t("unlocked_at")} -

    {formatDateTime(achievements[index].unlockTime)}

    -
    - )} + {achievements.map((achievement, index) => ( +
  • + {achievement.displayName} +
    +

    {achievement.displayName}

    +

    {achievement.description}

    + {achievement.unlockTime && ( +
    + {t("unlocked_at")} +

    {formatDateTime(achievement.unlockTime)}

    +
    + )}
  • ))}
); } -export function AchievementsContent({ otherUser }: AchievementsContentProps) { +export function AchievementsContent({ + otherUser, + comparedAchievements, +}: AchievementsContentProps) { const heroRef = useRef(null); const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); - const [backdropOpactiy, setBackdropOpacity] = useState(1); const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = useContext(gameDetailsContext); - const sortedAchievements = useMemo(() => { - if (!otherUser || otherUser.achievements.length === 0) return achievements!; - - return achievements!.sort((a, b) => { - const indexA = otherUser.achievements.findIndex( - (achievement) => achievement.name === a.name - ); - const indexB = otherUser.achievements.findIndex( - (achievement) => achievement.name === b.name - ); - return indexA - indexB; - }); - }, [achievements, otherUser]); - const dispatch = useAppDispatch(); - const { userDetails } = useUserDetails(); + const { userDetails, hasActiveSubscription } = useUserDetails(); useEffect(() => { if (gameTitle) { @@ -399,11 +237,6 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; const scrollY = (event.target as HTMLDivElement).scrollTop; - const opacity = Math.max( - 0, - 1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD) - ); - if (scrollY >= heroHeight && !isHeaderStuck) { setIsHeaderStuck(true); } @@ -411,8 +244,25 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { if (scrollY <= heroHeight && isHeaderStuck) { setIsHeaderStuck(false); } + }; - setBackdropOpacity(opacity); + const getProfileImage = ( + profileImageUrl: string | null, + displayName: string + ) => { + return ( +
+ {profileImageUrl ? ( + {displayName} + ) : ( + + )} +
+ ); }; if (!objectId || !shop || !gameTitle || !userDetails) return null; @@ -421,8 +271,9 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
{gameTitle} @@ -431,39 +282,86 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { onScroll={onScroll} className={styles.container} > -
-
+
+
+
+ + {gameTitle} + +
+
-
- {gameTitle} -
+ achievement.unlocked) + .length, + }} + isComparison={otherUser !== null} + /> + + {otherUser && }
-
- -
+ {otherUser && ( +
+
+
+ {hasActiveSubscription && ( +
+ {getProfileImage( + userDetails.profileImageUrl, + userDetails.displayName + )} +
+ )} +
+ {getProfileImage( + otherUser.profileImageUrl, + otherUser.displayName + )} +
+
+
+ )} +
- + {otherUser ? ( + + ) : ( + + )}
diff --git a/src/renderer/src/pages/achievements/achievements-skeleton.tsx b/src/renderer/src/pages/achievements/achievements-skeleton.tsx index f9ae81ac..f26f3951 100644 --- a/src/renderer/src/pages/achievements/achievements-skeleton.tsx +++ b/src/renderer/src/pages/achievements/achievements-skeleton.tsx @@ -4,7 +4,7 @@ import * as styles from "./achievements.css"; export function AchievementsSkeleton() { return (
-
+
diff --git a/src/renderer/src/pages/achievements/achievements.css.ts b/src/renderer/src/pages/achievements/achievements.css.ts index 24f1d507..c4b66384 100644 --- a/src/renderer/src/pages/achievements/achievements.css.ts +++ b/src/renderer/src/pages/achievements/achievements.css.ts @@ -2,7 +2,9 @@ import { SPACING_UNIT, vars } from "../../theme.css"; import { style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; -export const HERO_HEIGHT = 300; +export const HERO_HEIGHT = 150; +export const LOGO_HEIGHT = 100; +export const LOGO_MAX_WIDTH = 200; export const wrapper = style({ display: "flex", @@ -13,11 +15,11 @@ export const wrapper = style({ transition: "all ease 0.3s", }); -export const header = style({ - display: "flex", +export const hero = style({ + width: "100%", height: `${HERO_HEIGHT}px`, minHeight: `${HERO_HEIGHT}px`, - gap: `${SPACING_UNIT}px`, + display: "flex", flexDirection: "column", position: "relative", transition: "all ease 0.2s", @@ -29,27 +31,41 @@ export const header = style({ }, }); -export const hero = style({ - position: "absolute", - inset: "0", - borderRadius: "4px", - objectFit: "cover", - cursor: "pointer", +export const heroImage = style({ width: "100%", + height: `${HERO_HEIGHT}px`, + minHeight: `${HERO_HEIGHT}px`, + objectFit: "cover", + objectPosition: "top", transition: "all ease 0.2s", + position: "absolute", + zIndex: "0", + filter: "blur(5px)", + "@media": { + "(min-width: 1250px)": { + objectPosition: "center", + height: "350px", + minHeight: "350px", + }, + }, }); export const heroContent = style({ padding: `${SPACING_UNIT * 2}px`, - height: "100%", width: "100%", display: "flex", justifyContent: "space-between", - alignItems: "flex-end", + alignItems: "center", }); export const gameLogo = style({ - width: 300, + width: LOGO_MAX_WIDTH, + height: LOGO_HEIGHT, + objectFit: "contain", + transition: "all ease 0.2s", + ":hover": { + transform: "scale(1.05)", + }, }); export const container = style({ @@ -61,19 +77,13 @@ export const container = style({ zIndex: "1", }); -export const panel = recipe({ +export const tableHeader = recipe({ base: { width: "100%", - height: "100px", - minHeight: "100px", - padding: `${SPACING_UNIT * 2}px 0`, backgroundColor: vars.color.darkBackground, - display: "flex", - flexDirection: "row", transition: "all ease 0.2s", borderBottom: `solid 1px ${vars.color.border}`, position: "sticky", - overflow: "hidden", top: "0", zIndex: "1", }, @@ -145,7 +155,6 @@ export const achievementsProgressBar = style({ export const heroLogoBackdrop = style({ width: "100%", height: "100%", - background: "linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%)", position: "absolute", display: "flex", flexDirection: "column", @@ -180,8 +189,20 @@ export const listItemSkeleton = style({ }); export const profileAvatar = style({ - height: "65px", - width: "65px", + height: "54px", + width: "54px", + borderRadius: "4px", + display: "flex", + justifyContent: "center", + alignItems: "center", + backgroundColor: vars.color.background, + position: "relative", + objectFit: "cover", +}); + +export const profileAvatarSmall = style({ + height: "32px", + width: "32px", borderRadius: "4px", display: "flex", justifyContent: "center", diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx index a53eaba8..d15ba0b9 100644 --- a/src/renderer/src/pages/achievements/achievements.tsx +++ b/src/renderer/src/pages/achievements/achievements.tsx @@ -1,7 +1,7 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useUserDetails } from "@renderer/hooks"; -import type { GameShop, UserAchievement } from "@types"; -import { useEffect, useState } from "react"; +import type { ComparedAchievements, GameShop } from "@types"; +import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { vars } from "@renderer/theme.css"; import { @@ -18,14 +18,11 @@ export default function Achievements() { const shop = searchParams.get("shop"); const title = searchParams.get("title"); const userId = searchParams.get("userId"); - const displayName = searchParams.get("displayName"); - const profileImageUrl = searchParams.get("profileImageUrl"); const { userDetails } = useUserDetails(); - const [otherUserAchievements, setOtherUserAchievements] = useState< - UserAchievement[] | null - >(null); + const [comparedAchievements, setComparedAchievements] = + useState(null); const dispatch = useAppDispatch(); @@ -36,31 +33,34 @@ export default function Achievements() { }, [dispatch, title]); useEffect(() => { - setOtherUserAchievements(null); + setComparedAchievements(null); + if (userDetails?.id == userId) { - setOtherUserAchievements([]); return; } if (objectId && shop && userId) { window.electron - .getGameAchievements(objectId, shop as GameShop, userId) - .then((achievements) => { - setOtherUserAchievements(achievements); - }); + .getComparedUnlockedAchievements(objectId, shop as GameShop, userId) + .then(setComparedAchievements); } }, [objectId, shop, userId]); const otherUserId = userDetails?.id === userId ? null : userId; - const otherUser = otherUserId - ? { - userId: otherUserId, - displayName: displayName || "", - achievements: otherUserAchievements || [], - profileImageUrl: profileImageUrl || "", - } - : null; + const otherUser = useMemo(() => { + if (!otherUserId || !comparedAchievements) return null; + + return { + userId: otherUserId, + displayName: comparedAchievements.otherUser.displayName, + profileImageUrl: comparedAchievements.otherUser.profileImageUrl, + totalAchievementCount: + comparedAchievements.otherUser.totalAchievementCount, + unlockedAchievementCount: + comparedAchievements.otherUser.unlockedAchievementCount, + }; + }, [otherUserId, comparedAchievements]); return ( {({ isLoading, achievements }) => { + const showSkeleton = + isLoading || + achievements === null || + (otherUserId && comparedAchievements === null); + return ( - {isLoading || - achievements === null || - (otherUserId && otherUserAchievements === null) ? ( + {showSkeleton ? ( ) : ( - + )} ); diff --git a/src/renderer/src/pages/achievements/compared-achievement-list.tsx b/src/renderer/src/pages/achievements/compared-achievement-list.tsx new file mode 100644 index 00000000..6c0484c3 --- /dev/null +++ b/src/renderer/src/pages/achievements/compared-achievement-list.tsx @@ -0,0 +1,110 @@ +import type { ComparedAchievements } from "@types"; +import * as styles from "./achievements.css"; +import { CheckCircleIcon, LockIcon } from "@primer/octicons-react"; +import { useDate } from "@renderer/hooks"; +import { SPACING_UNIT } from "@renderer/theme.css"; + +export interface ComparedAchievementListProps { + achievements: ComparedAchievements; +} + +export function ComparedAchievementList({ + achievements, +}: ComparedAchievementListProps) { + const { formatDateTime } = useDate(); + + return ( +
    + {achievements.achievements.map((achievement, index) => ( +
  • +
    + {achievement.displayName} +
    +

    {achievement.displayName}

    +

    {achievement.description}

    +
    +
    + + {achievement.onwerUserStat ? ( + achievement.onwerUserStat.unlocked ? ( +
    + + + {formatDateTime(achievement.onwerUserStat.unlockTime!)} + +
    + ) : ( +
    + +
    + ) + ) : null} + + {achievement.otherUserStat.unlocked ? ( +
    + + + {formatDateTime(achievement.otherUserStat.unlockTime!)} + +
    + ) : ( +
    + +
    + )} +
  • + ))} +
+ ); +} diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 8d1fe8d6..a6a24850 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -24,6 +24,8 @@ const fakeAchievements: UserAchievement[] = [ hidden: false, description: "Chop down your first tree.", icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg", + icongray: + "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg", unlocked: true, unlockTime: Date.now(), }, @@ -32,6 +34,8 @@ const fakeAchievements: UserAchievement[] = [ name: "", hidden: false, icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg", + icongray: + "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg", unlocked: false, unlockTime: null, }, @@ -40,6 +44,8 @@ const fakeAchievements: UserAchievement[] = [ name: "", hidden: false, icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg", + icongray: + "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg", unlocked: false, unlockTime: null, }, diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 99e09889..a989b81d 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -46,7 +46,7 @@ export function ProfileContent() { }, [userProfile]); const buildUserGameDetailsPath = (game: UserGame) => { - if (!userProfile?.hasActiveSubscription) { + if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) { return buildGameDetailsPath({ ...game, objectId: game.objectId, @@ -56,8 +56,6 @@ export function ProfileContent() { const userParams = userProfile ? { userId: userProfile.id, - displayName: userProfile.displayName, - profileImageUrl: userProfile.profileImageUrl, } : undefined; @@ -174,55 +172,56 @@ export function ProfileContent() { {formatPlayTime(game.playTimeInSeconds)} - {userProfile.hasActiveSubscription && ( -
+ {userProfile.hasActiveSubscription && + game.achievementCount > 0 && (
- +
+ + + {game.unlockedAchievementCount} /{" "} + {game.achievementCount} + +
+ - {game.unlockedAchievementCount} /{" "} - {game.achievementCount} + {formatDownloadProgress( + game.unlockedAchievementCount / + game.achievementCount + )}
- - {formatDownloadProgress( + + game.achievementCount + } + className={styles.achievementsProgressBar} + />
- - -
- )} + )}