mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: replace notification window
This commit is contained in:
parent
a45e06efa3
commit
5d304f9e13
12 changed files with 61 additions and 296 deletions
|
@ -20,7 +20,7 @@ export const seedsPath = app.isPackaged
|
||||||
: path.join(__dirname, "..", "..", "seeds");
|
: path.join(__dirname, "..", "..", "seeds");
|
||||||
|
|
||||||
export const achievementSoundPath = app.isPackaged
|
export const achievementSoundPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, "resources", "achievement.wav")
|
? path.join(process.resourcesPath, "achievement.wav")
|
||||||
: path.join(__dirname, "..", "..", "resources", "achievement.wav");
|
: path.join(__dirname, "..", "..", "resources", "achievement.wav");
|
||||||
|
|
||||||
export const backupsPath = path.join(app.getPath("userData"), "Backups");
|
export const backupsPath = path.join(app.getPath("userData"), "Backups");
|
||||||
|
|
|
@ -102,7 +102,6 @@ app.whenReady().then(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
WindowManager.createNotificationWindow();
|
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,6 @@ import { UserPreferences } from "./entity";
|
||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
import { Toast } from "powertoast";
|
|
||||||
import { publishNewAchievementNotification } from "./services/notifications";
|
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
import("./events");
|
import("./events");
|
||||||
|
@ -51,12 +49,5 @@ userPreferencesRepository
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
})
|
})
|
||||||
.then((userPreferences) => {
|
.then((userPreferences) => {
|
||||||
publishNewAchievementNotification({
|
|
||||||
icon: "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/72850/c3a604f698d247b53d20f212e9f31a9ec707a180.jpg",
|
|
||||||
displayName: "Hydra has started",
|
|
||||||
totalAchievementCount: 75,
|
|
||||||
unlockedAchievementCount: 23,
|
|
||||||
});
|
|
||||||
|
|
||||||
loadState(userPreferences);
|
loadState(userPreferences);
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { achievementsLogger } from "../logger";
|
||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
import { IsNull, Not } from "typeorm";
|
import { IsNull, Not } from "typeorm";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
|
import { publishNewAchievementBulkNotification } from "../notifications";
|
||||||
|
|
||||||
const fileStats: Map<string, number> = new Map();
|
const fileStats: Map<string, number> = new Map();
|
||||||
const fltFiles: Map<string, Set<string>> = new Map();
|
const fltFiles: Map<string, Set<string>> = new Map();
|
||||||
|
@ -249,10 +250,9 @@ export class AchievementWatcherManager {
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
WindowManager.notificationWindow?.webContents.send(
|
publishNewAchievementBulkNotification(
|
||||||
"on-combined-achievements-unlocked",
|
totalNewAchievements,
|
||||||
totalNewGamesWithAchievements,
|
totalNewGamesWithAchievements
|
||||||
totalNewAchievements
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.hasFinishedMergingWithRemote = true;
|
this.hasFinishedMergingWithRemote = true;
|
||||||
|
|
|
@ -8,6 +8,10 @@ import { HydraApi } from "../hydra-api";
|
||||||
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
|
import {
|
||||||
|
publishNewAchievementBulkNotification as publishCombinedNewAchievementNotification,
|
||||||
|
publishNewAchievementNotification,
|
||||||
|
} from "../notifications";
|
||||||
|
|
||||||
const saveAchievementsOnLocal = async (
|
const saveAchievementsOnLocal = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
|
@ -82,6 +86,8 @@ export const mergeAchievements = async (
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
newAchievements.length &&
|
newAchievements.length &&
|
||||||
publishNotification &&
|
publishNotification &&
|
||||||
|
@ -107,16 +113,22 @@ export const mergeAchievements = async (
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowManager.notificationWindow?.webContents.send(
|
if (achievementsInfo.length > 1) {
|
||||||
"on-achievement-unlocked",
|
publishCombinedNewAchievementNotification(
|
||||||
game.objectID,
|
newAchievements.length,
|
||||||
game.shop,
|
1,
|
||||||
achievementsInfo
|
achievementsInfo[0].iconUrl
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
publishNewAchievementNotification({
|
||||||
|
displayName: achievementsInfo[0].displayName,
|
||||||
|
achievementIcon: achievementsInfo[0].iconUrl,
|
||||||
|
unlockedAchievementCount: mergedLocalAchievements.length,
|
||||||
|
totalAchievementCount: achievementsData.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
|
||||||
|
|
||||||
if (game.remoteId) {
|
if (game.remoteId) {
|
||||||
await HydraApi.put("/profile/games/achievements", {
|
await HydraApi.put("/profile/games/achievements", {
|
||||||
id: game.remoteId,
|
id: game.remoteId,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import axios from "axios";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import sound from "sound-play";
|
import sound from "sound-play";
|
||||||
import { achievementSoundPath } from "@main/constants";
|
import { achievementSoundPath } from "@main/constants";
|
||||||
|
import icon from "@resources/icon.png?asset";
|
||||||
|
|
||||||
const getGameIconNativeImage = async (gameId: number) => {
|
const getGameIconNativeImage = async (gameId: number) => {
|
||||||
try {
|
try {
|
||||||
|
@ -91,13 +92,46 @@ async function downloadImage(url: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const publishNewAchievementBulkNotification = async (
|
||||||
|
achievementCount,
|
||||||
|
gameCount,
|
||||||
|
achievementIcon?: string
|
||||||
|
) => {
|
||||||
|
const iconPath = achievementIcon
|
||||||
|
? await downloadImage(achievementIcon)
|
||||||
|
: icon;
|
||||||
|
|
||||||
|
new Notification({
|
||||||
|
title: "New achievement unlocked",
|
||||||
|
body: t("new_achievements_unlocked", {
|
||||||
|
ns: "achievement",
|
||||||
|
gameCount,
|
||||||
|
achievementCount,
|
||||||
|
}),
|
||||||
|
icon: iconPath,
|
||||||
|
silent: true,
|
||||||
|
toastXml: toXmlString({
|
||||||
|
title: "New achievement unlocked",
|
||||||
|
message: t("new_achievements_unlocked", {
|
||||||
|
ns: "achievement",
|
||||||
|
gameCount,
|
||||||
|
achievementCount,
|
||||||
|
}),
|
||||||
|
icon: iconPath,
|
||||||
|
silent: true,
|
||||||
|
}),
|
||||||
|
}).show();
|
||||||
|
|
||||||
|
sound.play(achievementSoundPath);
|
||||||
|
};
|
||||||
|
|
||||||
export const publishNewAchievementNotification = async (achievement: {
|
export const publishNewAchievementNotification = async (achievement: {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
icon: string;
|
achievementIcon: string;
|
||||||
unlockedAchievementCount: number;
|
unlockedAchievementCount: number;
|
||||||
totalAchievementCount: number;
|
totalAchievementCount: number;
|
||||||
}) => {
|
}) => {
|
||||||
const iconPath = await downloadImage(achievement.icon);
|
const iconPath = await downloadImage(achievement.achievementIcon);
|
||||||
|
|
||||||
new Notification({
|
new Notification({
|
||||||
title: "New achievement unlocked",
|
title: "New achievement unlocked",
|
||||||
|
|
|
@ -20,7 +20,6 @@ import UserAgent from "user-agents";
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||||
public static notificationWindow: Electron.BrowserWindow | null = null;
|
|
||||||
|
|
||||||
private static loadMainWindowURL(hash = "") {
|
private static loadMainWindowURL(hash = "") {
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
@ -39,21 +38,6 @@ export class WindowManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static loadNotificationWindowURL() {
|
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
|
||||||
this.notificationWindow?.loadURL(
|
|
||||||
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.notificationWindow?.loadFile(
|
|
||||||
path.join(__dirname, "../renderer/index.html"),
|
|
||||||
{
|
|
||||||
hash: "achievement-notification",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createMainWindow() {
|
public static createMainWindow() {
|
||||||
if (this.mainWindow) return;
|
if (this.mainWindow) return;
|
||||||
|
|
||||||
|
@ -151,32 +135,6 @@ export class WindowManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createNotificationWindow() {
|
|
||||||
this.notificationWindow = new BrowserWindow({
|
|
||||||
transparent: true,
|
|
||||||
maximizable: false,
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
minimizable: false,
|
|
||||||
focusable: false,
|
|
||||||
skipTaskbar: true,
|
|
||||||
frame: false,
|
|
||||||
width: 350,
|
|
||||||
height: 104,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
|
||||||
sandbox: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
|
||||||
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
|
||||||
// visibleOnFullScreen: true,
|
|
||||||
// });
|
|
||||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
|
||||||
this.loadNotificationWindowURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static openAuthWindow() {
|
public static openAuthWindow() {
|
||||||
if (this.mainWindow) {
|
if (this.mainWindow) {
|
||||||
const authWindow = new BrowserWindow({
|
const authWindow = new BrowserWindow({
|
||||||
|
|
|
@ -51,35 +51,6 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
getGameStats: (objectId: string, shop: GameShop) =>
|
getGameStats: (objectId: string, shop: GameShop) =>
|
||||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||||
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
||||||
onAchievementUnlocked: (
|
|
||||||
cb: (
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop,
|
|
||||||
achievements?: { displayName: string; iconUrl: string }[]
|
|
||||||
) => void
|
|
||||||
) => {
|
|
||||||
const listener = (
|
|
||||||
_event: Electron.IpcRendererEvent,
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop,
|
|
||||||
achievements?: { displayName: string; iconUrl: string }[]
|
|
||||||
) => cb(objectId, shop, achievements);
|
|
||||||
ipcRenderer.on("on-achievement-unlocked", listener);
|
|
||||||
return () =>
|
|
||||||
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
|
||||||
},
|
|
||||||
onCombinedAchievementsUnlocked: (
|
|
||||||
cb: (gameCount: number, achievementsCount: number) => void
|
|
||||||
) => {
|
|
||||||
const listener = (
|
|
||||||
_event: Electron.IpcRendererEvent,
|
|
||||||
gameCount: number,
|
|
||||||
achievementCount: number
|
|
||||||
) => cb(gameCount, achievementCount);
|
|
||||||
ipcRenderer.on("on-combined-achievements-unlocked", listener);
|
|
||||||
return () =>
|
|
||||||
ipcRenderer.removeListener("on-combined-achievements-unlocked", listener);
|
|
||||||
},
|
|
||||||
onUpdateAchievements: (
|
onUpdateAchievements: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
|
10
src/renderer/src/declaration.d.ts
vendored
10
src/renderer/src/declaration.d.ts
vendored
|
@ -66,16 +66,6 @@ declare global {
|
||||||
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||||
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
||||||
getTrendingGames: () => Promise<TrendingGame[]>;
|
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: (
|
onUpdateAchievements: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { App } from "./app";
|
||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
|
|
||||||
|
|
||||||
import "./workers";
|
import "./workers";
|
||||||
import { RepacksContextProvider } from "./context";
|
import { RepacksContextProvider } from "./context";
|
||||||
|
@ -97,10 +96,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
element={<SuspenseWrapper Component={Achievements} />}
|
element={<SuspenseWrapper Component={Achievements} />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
|
||||||
path="/achievement-notification"
|
|
||||||
Component={AchievementNotification}
|
|
||||||
/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</RepacksContextProvider>
|
</RepacksContextProvider>
|
||||||
|
|
|
@ -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",
|
|
||||||
});
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue