From 5d304f9e1354c3c6846ad0698853c04f4f1d403d Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:33:01 -0300 Subject: [PATCH] feat: replace notification window --- src/main/constants.ts | 2 +- src/main/index.ts | 1 - src/main/main.ts | 9 -- .../achievement-watcher-manager.ts | 8 +- .../achievements/merge-achievements.ts | 28 +++- src/main/services/notifications.ts | 38 ++++- src/main/services/window-manager.ts | 42 ------ src/preload/index.ts | 29 ---- src/renderer/src/declaration.d.ts | 10 -- src/renderer/src/main.tsx | 5 - .../achievement-notification.css.ts | 44 ------ .../notification/achievement-notification.tsx | 141 ------------------ 12 files changed, 61 insertions(+), 296 deletions(-) delete mode 100644 src/renderer/src/pages/achievements/notification/achievement-notification.css.ts delete mode 100644 src/renderer/src/pages/achievements/notification/achievement-notification.tsx diff --git a/src/main/constants.ts b/src/main/constants.ts index 9ab63f93..2b92b719 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -20,7 +20,7 @@ export const seedsPath = app.isPackaged : path.join(__dirname, "..", "..", "seeds"); export const achievementSoundPath = app.isPackaged - ? path.join(process.resourcesPath, "resources", "achievement.wav") + ? path.join(process.resourcesPath, "achievement.wav") : path.join(__dirname, "..", "..", "resources", "achievement.wav"); export const backupsPath = path.join(app.getPath("userData"), "Backups"); diff --git a/src/main/index.ts b/src/main/index.ts index 1b1629ef..9127b881 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -102,7 +102,6 @@ app.whenReady().then(async () => { } WindowManager.createMainWindow(); - WindowManager.createNotificationWindow(); WindowManager.createSystemTray(userPreferences?.language || "en"); }); diff --git a/src/main/main.ts b/src/main/main.ts index 2d38ae5c..69bc62e0 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -12,8 +12,6 @@ import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; -import { Toast } from "powertoast"; -import { publishNewAchievementNotification } from "./services/notifications"; const loadState = async (userPreferences: UserPreferences | null) => { import("./events"); @@ -51,12 +49,5 @@ userPreferencesRepository where: { id: 1 }, }) .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); }); diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 610556b9..973ac3a7 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -14,6 +14,7 @@ import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; import { IsNull, Not } from "typeorm"; import { WindowManager } from "../window-manager"; +import { publishNewAchievementBulkNotification } from "../notifications"; const fileStats: Map = new Map(); const fltFiles: Map> = new Map(); @@ -249,10 +250,9 @@ export class AchievementWatcherManager { 0 ); - WindowManager.notificationWindow?.webContents.send( - "on-combined-achievements-unlocked", - totalNewGamesWithAchievements, - totalNewAchievements + publishNewAchievementBulkNotification( + totalNewAchievements, + totalNewGamesWithAchievements ); this.hasFinishedMergingWithRemote = true; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index a00ed639..0fb7cb96 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -8,6 +8,10 @@ import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; import { Game } from "@main/entity"; import { achievementsLogger } from "../logger"; +import { + publishNewAchievementBulkNotification as publishCombinedNewAchievementNotification, + publishNewAchievementNotification, +} from "../notifications"; const saveAchievementsOnLocal = async ( objectId: string, @@ -82,6 +86,8 @@ export const mergeAchievements = async ( }; }); + const mergedLocalAchievements = unlockedAchievements.concat(newAchievements); + if ( newAchievements.length && publishNotification && @@ -107,16 +113,22 @@ export const mergeAchievements = async ( }; }); - WindowManager.notificationWindow?.webContents.send( - "on-achievement-unlocked", - game.objectID, - game.shop, - achievementsInfo - ); + if (achievementsInfo.length > 1) { + publishCombinedNewAchievementNotification( + newAchievements.length, + 1, + 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) { await HydraApi.put("/profile/games/achievements", { id: game.remoteId, diff --git a/src/main/services/notifications.ts b/src/main/services/notifications.ts index d5ecc756..c3473cd1 100644 --- a/src/main/services/notifications.ts +++ b/src/main/services/notifications.ts @@ -10,6 +10,7 @@ import axios from "axios"; import path from "node:path"; import sound from "sound-play"; import { achievementSoundPath } from "@main/constants"; +import icon from "@resources/icon.png?asset"; const getGameIconNativeImage = async (gameId: number) => { 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: { displayName: string; - icon: string; + achievementIcon: string; unlockedAchievementCount: number; totalAchievementCount: number; }) => { - const iconPath = await downloadImage(achievement.icon); + const iconPath = await downloadImage(achievement.achievementIcon); new Notification({ title: "New achievement unlocked", diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index e1dc4dfc..3063881f 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -20,7 +20,6 @@ import UserAgent from "user-agents"; export class WindowManager { public static mainWindow: Electron.BrowserWindow | null = null; - public static notificationWindow: Electron.BrowserWindow | null = null; private static loadMainWindowURL(hash = "") { // 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() { 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() { if (this.mainWindow) { const authWindow = new BrowserWindow({ diff --git a/src/preload/index.ts b/src/preload/index.ts index 90c50763..6e513672 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -51,35 +51,6 @@ contextBridge.exposeInMainWorld("electron", { getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), 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: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 929b00e9..ddb254dd 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -66,16 +66,6 @@ declare global { searchGameRepacks: (query: string) => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; getTrendingGames: () => Promise; - 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, diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 11003295..581e3ce8 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -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={} /> - diff --git a/src/renderer/src/pages/achievements/notification/achievement-notification.css.ts b/src/renderer/src/pages/achievements/notification/achievement-notification.css.ts deleted file mode 100644 index ba9469bb..00000000 --- a/src/renderer/src/pages/achievements/notification/achievement-notification.css.ts +++ /dev/null @@ -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", -}); diff --git a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx deleted file mode 100644 index cb1d695c..00000000 --- a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx +++ /dev/null @@ -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([]); - const [currentAchievement, setCurrentAchievement] = - useState(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 ( -
-
- {currentAchievement.displayName} -
-

{t("achievement_unlocked")}

-

{currentAchievement.displayName}

-
-
-
- ); -}