feat: replace notification window

This commit is contained in:
Zamitto 2024-10-29 23:33:01 -03:00
parent a45e06efa3
commit 5d304f9e13
12 changed files with 61 additions and 296 deletions

View file

@ -66,16 +66,6 @@ declare global {
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
getTrendingGames: () => Promise<TrendingGame[]>;
onAchievementUnlocked: (
cb: (
objectId: string,
shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[]
) => void
) => () => Electron.IpcRenderer;
onCombinedAchievementsUnlocked: (
cb: (gameCount: number, achievementCount: number) => void
) => () => Electron.IpcRenderer;
onUpdateAchievements: (
objectId: string,
shop: GameShop,

View file

@ -19,7 +19,6 @@ import { App } from "./app";
import { store } from "./store";
import resources from "@locales";
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
import "./workers";
import { RepacksContextProvider } from "./context";
@ -97,10 +96,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
element={<SuspenseWrapper Component={Achievements} />}
/>
</Route>
<Route
path="/achievement-notification"
Component={AchievementNotification}
/>
</Routes>
</HashRouter>
</RepacksContextProvider>

View file

@ -1,44 +0,0 @@
import { recipe } from "@vanilla-extract/recipes";
import { vars } from "../../../theme.css";
import { keyframes, style } from "@vanilla-extract/css";
const animationIn = keyframes({
"0%": { transform: `translateY(-240px)` },
"100%": { transform: "translateY(0)" },
});
const animationOut = keyframes({
"0%": { transform: `translateY(0)` },
"100%": { transform: "translateY(-240px)" },
});
export const container = recipe({
base: {
marginTop: "24px",
marginLeft: "24px",
animationDuration: "1.0s",
height: "60px",
display: "flex",
},
variants: {
closing: {
true: {
animationName: animationOut,
transform: "translateY(-240px)",
},
false: {
animationName: animationIn,
transform: "translateY(0)",
},
},
},
});
export const content = style({
display: "flex",
flexDirection: "row",
gap: "8px",
alignItems: "center",
background: vars.color.background,
paddingRight: "8px",
});

View file

@ -1,141 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import achievementSound from "@renderer/assets/audio/achievement.wav";
import { useTranslation } from "react-i18next";
import * as styles from "./achievement-notification.css";
interface AchievementInfo {
displayName: string;
iconUrl: string;
}
const NOTIFICATION_TIMEOUT = 4000;
export function AchievementNotification() {
const { t } = useTranslation("achievement");
const [isClosing, setIsClosing] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
const [currentAchievement, setCurrentAchievement] =
useState<AchievementInfo | null>(null);
const achievementAnimation = useRef(-1);
const closingAnimation = useRef(-1);
const visibleAnimation = useRef(-1);
const playAudio = useCallback(() => {
const audio = new Audio(achievementSound);
audio.volume = 0.2;
audio.play();
}, []);
useEffect(() => {
const unsubscribe = window.electron.onCombinedAchievementsUnlocked(
(gameCount, achievementCount) => {
if (gameCount === 0 || achievementCount === 0) return;
setAchievements([
{
displayName: t("new_achievements_unlocked", {
gameCount,
achievementCount,
}),
iconUrl:
"https://avatars.githubusercontent.com/u/164102380?s=400&u=01a13a7b4f0c642f7e547b8e1d70440ea06fa750&v=4",
},
]);
playAudio();
}
);
return () => {
unsubscribe();
};
}, [playAudio]);
useEffect(() => {
const unsubscribe = window.electron.onAchievementUnlocked(
(_object, _shop, achievements) => {
if (!achievements || !achievements.length) return;
setAchievements((ach) => ach.concat(achievements));
playAudio();
}
);
return () => {
unsubscribe();
};
}, [playAudio]);
const hasAchievementsPending = achievements.length > 0;
const startAnimateClosing = useCallback(() => {
cancelAnimationFrame(closingAnimation.current);
cancelAnimationFrame(visibleAnimation.current);
cancelAnimationFrame(achievementAnimation.current);
setIsClosing(true);
const zero = performance.now();
closingAnimation.current = requestAnimationFrame(
function animateClosing(time) {
if (time - zero <= 1000) {
closingAnimation.current = requestAnimationFrame(animateClosing);
} else {
setIsVisible(false);
}
}
);
}, []);
useEffect(() => {
if (hasAchievementsPending) {
setIsClosing(false);
setIsVisible(true);
let zero = performance.now();
cancelAnimationFrame(closingAnimation.current);
cancelAnimationFrame(visibleAnimation.current);
cancelAnimationFrame(achievementAnimation.current);
achievementAnimation.current = requestAnimationFrame(
function animateLock(time) {
if (time - zero > NOTIFICATION_TIMEOUT) {
zero = performance.now();
setAchievements((ach) => ach.slice(1));
}
achievementAnimation.current = requestAnimationFrame(animateLock);
}
);
} else {
startAnimateClosing();
}
}, [hasAchievementsPending]);
useEffect(() => {
if (achievements.length) {
setCurrentAchievement(achievements[0]);
}
}, [achievements]);
if (!isVisible || !currentAchievement) return null;
return (
<div className={styles.container({ closing: isClosing })}>
<div className={styles.content}>
<img
src={currentAchievement.iconUrl}
alt={currentAchievement.displayName}
style={{ flex: 1, width: "60px" }}
/>
<div>
<p>{t("achievement_unlocked")}</p>
<p>{currentAchievement.displayName}</p>
</div>
</div>
</div>
);
}