diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 4ee747da..76a5d830 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -107,10 +107,10 @@ export class WindowManager { focusable: false, skipTaskbar: true, frame: false, - width: 240, - height: 60, - x: 25, - y: 25, + width: 350, + height: 104, + x: 0, + y: 0, webPreferences: { preload: path.join(__dirname, "../preload/index.mjs"), sandbox: false, diff --git a/src/renderer/src/pages/achievement/achievement.css.ts b/src/renderer/src/pages/achievement/achievement.css.ts new file mode 100644 index 00000000..d5cb180b --- /dev/null +++ b/src/renderer/src/pages/achievement/achievement.css.ts @@ -0,0 +1,44 @@ +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", +}); diff --git a/src/renderer/src/pages/achievement/achievement.tsx b/src/renderer/src/pages/achievement/achievement.tsx index 27559e38..0ab5ac6a 100644 --- a/src/renderer/src/pages/achievement/achievement.tsx +++ b/src/renderer/src/pages/achievement/achievement.tsx @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import achievementSound from "@renderer/assets/audio/achievement.wav"; import { useTranslation } from "react-i18next"; -import { vars } from "@renderer/theme.css"; +import * as styles from "./achievement.css"; interface AchievementInfo { displayName: string; @@ -11,8 +11,16 @@ interface AchievementInfo { export function Achievement() { const { t } = useTranslation("achievement"); + const [isClosing, setIsClosing] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [achievements, setAchievements] = useState([]); + const [currentAchievement, setCurrentAchievement] = + useState(null); + const achievementAnimation = useRef(-1); + const closingAnimation = useRef(-1); + const visibleAnimation = useRef(-1); const audio = useMemo(() => { const audio = new Audio(achievementSound); @@ -24,11 +32,9 @@ export function Achievement() { useEffect(() => { const unsubscribe = window.electron.onAchievementUnlocked( (_object, _shop, achievements) => { - if (!achievements) return; + if (!achievements || !achievements.length) return; - if (achievements.length) { - setAchievements((ach) => ach.concat(achievements)); - } + setAchievements((ach) => ach.concat(achievements)); audio.play(); } @@ -41,12 +47,37 @@ export function Achievement() { 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 > 3000) { + if (time - zero > 2500) { zero = performance.now(); setAchievements((ach) => ach.slice(1)); } @@ -54,30 +85,30 @@ export function Achievement() { } ); } else { - cancelAnimationFrame(achievementAnimation.current); + startAnimateClosing(); } }, [hasAchievementsPending]); - if (!hasAchievementsPending) return null; + useEffect(() => { + if (achievements.length) { + setCurrentAchievement(achievements[0]); + } + }, [achievements]); + + if (!isVisible || !currentAchievement) return null; return ( -
- {achievements[0].displayName} -
-

{t("achievement_unlocked")}

-

{achievements[0].displayName}

+
+
+ {currentAchievement.displayName} +
+

{t("achievement_unlocked")}

+

{currentAchievement.displayName}

+
);