Merge branch 'feature/game-achievements' into chore/test-preview

# Conflicts:
#	src/main/services/window-manager.ts
#	src/renderer/src/context/game-details/game-details.context.tsx
#	src/renderer/src/declaration.d.ts
#	src/types/index.ts
#	yarn.lock
This commit is contained in:
Zamitto 2024-10-02 15:16:43 -03:00
commit ef4844b8c0
44 changed files with 3311 additions and 1446 deletions

Binary file not shown.

View file

@ -12,6 +12,7 @@ import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
import type {
Game,
GameAchievement,
GameRepack,
GameShop,
GameStats,
@ -36,6 +37,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
showRepacksModal: false,
showGameOptionsModal: false,
stats: null,
achievements: [],
hasNSFWContentBlocked: false,
setGameColor: () => {},
selectGameExecutable: async () => null,
@ -62,6 +64,7 @@ export function GameDetailsContextProvider({
shop,
}: GameDetailsContextProps) {
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
const [game, setGame] = useState<Game | null>(null);
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
@ -133,6 +136,15 @@ export function GameDetailsContextProvider({
setStats(result);
});
window.electron
.getGameAchievements(objectId!, shop as GameShop)
.then((achievements) => {
setAchievements(achievements);
})
.catch(() => {
// TODO: handle user not logged in error
});
updateGame();
}, [updateGame, dispatch, gameTitle, objectId, shop, i18n.language]);
@ -141,6 +153,7 @@ export function GameDetailsContextProvider({
setGame(null);
setIsLoading(true);
setisGameRunning(false);
setAchievements([]);
dispatch(setHeaderTitle(gameTitle));
}, [objectId, gameTitle, dispatch]);
@ -161,6 +174,23 @@ export function GameDetailsContextProvider({
};
}, [game?.id, isGameRunning, updateGame]);
useEffect(() => {
const unsubscribe = window.electron.onAchievementUnlocked(
(objectId, shop) => {
if (objectId !== objectId || shop !== shop) return;
window.electron
.getGameAchievements(objectId!, shop as GameShop)
.then(setAchievements)
.catch(() => {});
}
);
return () => {
unsubscribe();
};
}, [objectId, shop]);
const getDownloadsPath = async () => {
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
return window.electron.getDefaultDownloadsPath();
@ -204,6 +234,7 @@ export function GameDetailsContextProvider({
showGameOptionsModal,
showRepacksModal,
stats,
achievements,
hasNSFWContentBlocked,
setHasNSFWContentBlocked,
setGameColor,

View file

@ -1,5 +1,6 @@
import type {
Game,
GameAchievement,
GameRepack,
GameShop,
GameStats,
@ -19,6 +20,7 @@ export interface GameDetailsContext {
showRepacksModal: boolean;
showGameOptionsModal: boolean;
stats: GameStats | null;
achievements: GameAchievement[];
hasNSFWContentBlocked: boolean;
setGameColor: React.Dispatch<React.SetStateAction<string>>;
selectGameExecutable: () => Promise<string | null>;

View file

@ -25,6 +25,7 @@ import type {
UserStats,
UserDetails,
FriendRequestSync,
GameAchievement,
GameArtifact,
LudusaviBackup,
} from "@types";
@ -68,6 +69,17 @@ declare global {
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
getTrendingGames: () => Promise<TrendingGame[]>;
getGameAchievements: (
objectId: string,
shop: GameShop
) => Promise<GameAchievement[]>;
onAchievementUnlocked: (
cb: (
objectId: string,
shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[]
) => void
) => () => Electron.IpcRenderer;
/* Library */
addGameToLibrary: (

View file

@ -1,4 +1,4 @@
import { formatDistance, subMilliseconds } from "date-fns";
import { format, formatDistance, subMilliseconds } from "date-fns";
import type { FormatDistanceOptions } from "date-fns";
import {
ptBR,
@ -67,5 +67,13 @@ export function useDate() {
return "";
}
},
format: (timestamp: number): string => {
const locale = getDateLocale();
return format(
timestamp,
locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy - HH:mm"
);
},
};
}

View file

@ -28,6 +28,7 @@ import {
import { store } from "./store";
import resources from "@locales";
import { Achievemnt } from "./pages/achievement/achievement";
import "./workers";
import { RepacksContextProvider } from "./context";
@ -69,6 +70,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<Route path="/settings" Component={Settings} />
<Route path="/profile/:userId" Component={Profile} />
</Route>
<Route path="/achievement-notification" Component={Achievemnt} />
</Routes>
</HashRouter>
</RepacksContextProvider>

View file

@ -0,0 +1,64 @@
import { useEffect, useMemo, useState } from "react";
import achievementSound from "@renderer/assets/audio/achievement.wav";
import { useTranslation } from "react-i18next";
export function Achievemnt() {
const { t } = useTranslation("achievement");
const [achievementInfo, setAchievementInfo] = useState<{
displayName: string;
icon: string;
} | null>(null);
const audio = useMemo(() => {
const audio = new Audio(achievementSound);
audio.volume = 0.2;
audio.preload = "auto";
return audio;
}, []);
useEffect(() => {
const unsubscribe = window.electron.onAchievementUnlocked(
(_object, _shop, achievements) => {
if (!achievements) return;
if (achievements.length) {
const achievement = achievements[0];
setAchievementInfo({
displayName: achievement.displayName,
icon: achievement.iconUrl,
});
}
audio.play();
}
);
return () => {
unsubscribe();
};
}, [audio]);
if (!achievementInfo) return <p>Nada</p>;
return (
<div
style={{
display: "flex",
flexDirection: "row",
gap: "8px",
alignItems: "center",
}}
>
<img
src={achievementInfo.icon}
alt={achievementInfo.displayName}
style={{ width: 60, height: 60 }}
/>
<div>
<p>{t("achievement_unlocked")}</p>
<p>{achievementInfo.displayName}</p>
</div>
</div>
);
}

View file

@ -5,8 +5,9 @@ import { Button } from "@renderer/components";
import * as styles from "./sidebar.css";
import { gameDetailsContext } from "@renderer/context";
import { useFormat } from "@renderer/hooks";
import { useDate, useFormat } from "@renderer/hooks";
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import { SPACING_UNIT } from "@renderer/theme.css";
export function Sidebar() {
const [_howLongToBeat, _setHowLongToBeat] = useState<{
@ -17,9 +18,11 @@ export function Sidebar() {
const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
const { gameTitle, shopDetails, stats } = useContext(gameDetailsContext);
const { gameTitle, shopDetails, stats, achievements } =
useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
const { format } = useDate();
const { numberFormatter } = useFormat();
@ -45,6 +48,47 @@ export function Sidebar() {
isLoading={howLongToBeat.isLoading}
/> */}
{achievements.length > 0 && (
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT}px`,
}}
>
{achievements.map((achievement, index) => (
<div
key={index}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
}}
title={achievement.description}
>
<img
style={{
height: "72px",
width: "72px",
filter: achievement.unlocked ? "none" : "grayscale(100%)",
}}
src={
achievement.unlocked ? achievement.icon : achievement.icongray
}
alt={achievement.displayName}
loading="lazy"
/>
<div>
<p>{achievement.displayName}</p>
{achievement.unlockTime && format(achievement.unlockTime)}
</div>
</div>
))}
</div>
)}
{stats && (
<>
<div