diff --git a/src/main/events/index.ts b/src/main/events/index.ts index acc589f9..ce41b369 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -86,6 +86,7 @@ import "./cloud-save/upload-save-game"; import "./cloud-save/delete-game-artifact"; import "./cloud-save/select-game-backup-path"; import "./notifications/publish-new-repacks-notification"; +import "./notifications/update-achievement-notification-window-position"; import "./themes/add-custom-theme"; import "./themes/delete-custom-theme"; import "./themes/get-all-custom-themes"; diff --git a/src/main/events/notifications/update-achievement-notification-window-position.ts b/src/main/events/notifications/update-achievement-notification-window-position.ts new file mode 100644 index 00000000..3ba6d660 --- /dev/null +++ b/src/main/events/notifications/update-achievement-notification-window-position.ts @@ -0,0 +1,19 @@ +import { registerEvent } from "../register-event"; +import { WindowManager } from "@main/services"; + +const updateAchievementCustomNotificationWindowPosition = async ( + _event: Electron.IpcMainInvokeEvent +) => { + const { x, y } = await WindowManager.getNotificationWindowPosition(); + + WindowManager.notificationWindow?.setPosition(x, y); + + WindowManager.notificationWindow?.webContents.send( + "on-test-achievement-notification" + ); +}; + +registerEvent( + "updateAchievementCustomNotificationWindowPosition", + updateAchievementCustomNotificationWindowPosition +); diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 8fff5033..a06d9652 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -66,7 +66,7 @@ export const mergeAchievements = async ( const newAchievements = [...newAchievementsMap.values()] .filter((achievement) => { - return !unlockedAchievements.slice(1).some((localAchievement) => { + return !unlockedAchievements.some((localAchievement) => { return ( localAchievement.name.toUpperCase() === achievement.name.toUpperCase() ); diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index ea7afa82..5c080c09 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -6,6 +6,7 @@ import { Tray, app, nativeImage, + screen, shell, } from "electron"; import { is } from "@electron-toolkit/utils"; @@ -275,7 +276,74 @@ export class WindowManager { } } - public static createNotificationWindow() { + private static readonly NOTIFICATION_WINDOW_WIDTH = 350; + private static readonly NOTIFICATION_WINDOW_HEIGHT = 104; + + public static async getNotificationWindowPosition() { + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); + + const display = screen.getPrimaryDisplay(); + const { width, height } = display.workAreaSize; + + if ( + userPreferences?.achievementCustomNotificationPosition === "bottom_center" + ) { + return { + x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2, + y: height - this.NOTIFICATION_WINDOW_HEIGHT, + }; + } + + if ( + userPreferences?.achievementCustomNotificationPosition === "bottom_right" + ) { + return { + x: width - this.NOTIFICATION_WINDOW_WIDTH, + y: height - this.NOTIFICATION_WINDOW_HEIGHT, + }; + } + + if ( + userPreferences?.achievementCustomNotificationPosition === "top_center" + ) { + return { + x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2, + y: 0, + }; + } + + if ( + userPreferences?.achievementCustomNotificationPosition === "bottom_left" + ) { + return { + x: 0, + y: height - this.NOTIFICATION_WINDOW_HEIGHT, + }; + } + + if ( + userPreferences?.achievementCustomNotificationPosition === "top_right" + ) { + return { + x: width - this.NOTIFICATION_WINDOW_WIDTH, + y: 0, + }; + } + + return { + x: 0, + y: 0, + }; + } + + public static async createNotificationWindow() { + const { x, y } = await this.getNotificationWindowPosition(); + this.notificationWindow = new BrowserWindow({ transparent: true, maximizable: false, @@ -284,10 +352,10 @@ export class WindowManager { focusable: false, skipTaskbar: true, frame: false, - width: 350, - height: 104, - x: 0, - y: 0, + width: this.NOTIFICATION_WINDOW_WIDTH, + height: this.NOTIFICATION_WINDOW_HEIGHT, + x, + y, webPreferences: { preload: path.join(__dirname, "../preload/index.mjs"), sandbox: false, @@ -299,6 +367,12 @@ export class WindowManager { // }); this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1); this.loadNotificationWindowURL(); + + this.notificationWindow.once("ready-to-show", () => { + if (isStaging) { + this.notificationWindow?.webContents.openDevTools(); + } + }); } public static openEditorWindow(themeId: string) { diff --git a/src/preload/index.ts b/src/preload/index.ts index 3edf6b0c..33384289 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -207,35 +207,6 @@ contextBridge.exposeInMainWorld("electron", { return () => ipcRenderer.removeListener("on-library-batch-complete", listener); }, - 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); - }, onExtractionComplete: (cb: (shop: GameShop, objectId: string) => void) => { const listener = ( _event: Electron.IpcRendererEvent, @@ -433,6 +404,43 @@ contextBridge.exposeInMainWorld("electron", { /* Notifications */ publishNewRepacksNotification: (newRepacksCount: number) => ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount), + 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); + }, + onTestAchievementNotification: (cb: () => void) => { + const listener = (_event: Electron.IpcRendererEvent) => cb(); + ipcRenderer.on("on-test-achievement-notification", listener); + return () => + ipcRenderer.removeListener("on-test-achievement-notification", listener); + }, + updateAchievementCustomNotificationWindowPosition: () => + ipcRenderer.invoke("updateAchievementCustomNotificationWindowPosition"), /* Themes */ addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme), diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 28032d9a..eaa4cfcf 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -175,16 +175,6 @@ declare global { minimized: boolean; }) => Promise; extractGameDownload: (shop: GameShop, objectId: string) => Promise; - onAchievementUnlocked: ( - cb: ( - objectId: string, - shop: GameShop, - achievements?: { displayName: string; iconUrl: string }[] - ) => void - ) => () => Electron.IpcRenderer; - onCombinedAchievementsUnlocked: ( - cb: (gameCount: number, achievementCount: number) => void - ) => () => Electron.IpcRenderer; onExtractionComplete: ( cb: (shop: GameShop, objectId: string) => void ) => () => Electron.IpcRenderer; @@ -331,6 +321,18 @@ declare global { /* Notifications */ publishNewRepacksNotification: (newRepacksCount: number) => Promise; + onAchievementUnlocked: ( + cb: ( + objectId: string, + shop: GameShop, + achievements?: { displayName: string; iconUrl: string }[] + ) => void + ) => () => Electron.IpcRenderer; + onCombinedAchievementsUnlocked: ( + cb: (gameCount: number, achievementCount: number) => void + ) => () => Electron.IpcRenderer; + onTestAchievementNotification: (cb: () => void) => Electron.IpcRenderer; + updateAchievementCustomNotificationWindowPosition: () => Promise; /* Themes */ addCustomTheme: (theme: Theme) => Promise; diff --git a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx index 9aac12ad..49f06888 100644 --- a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx +++ b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx @@ -72,6 +72,26 @@ export function AchievementNotification() { }; }, [playAudio]); + useEffect(() => { + const unsubscribe = window.electron.onTestAchievementNotification(() => { + setAchievements((ach) => + ach.concat([ + { + displayName: "Test Achievement", + iconUrl: + "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fc.tenor.com%2FRwKr7hVnXREAAAAC%2Fnyan-cat.gif&f=1&nofb=1&ipt=706fd8b00cbfb5b2d2621603834d5f32c0f34cce7113de228d2fcc2247a80318", + }, + ]) + ); + + playAudio(); + }); + + return () => { + unsubscribe(); + }; + }, [playAudio]); + const hasAchievementsPending = achievements.length > 0; const startAnimateClosing = useCallback(() => { diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index 14faf883..b04c57cc 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import { TextField, Button, @@ -14,6 +14,7 @@ import { settingsContext } from "@renderer/context"; import "./settings-general.scss"; import { DesktopDownloadIcon } from "@primer/octicons-react"; import { logger } from "@renderer/logger"; +import { AchievementCustomNotificationPosition } from "@types"; interface LanguageOption { option: string; @@ -36,10 +37,12 @@ export function SettingsGeneral() { downloadsPath: "", downloadNotificationsEnabled: false, repackUpdatesNotificationsEnabled: false, - achievementNotificationsEnabled: false, friendRequestNotificationsEnabled: false, + achievementNotificationsEnabled: false, + achievementCustomNotificationsEnabled: true, + achievementCustomNotificationPosition: + "top_left" as AchievementCustomNotificationPosition, language: "", - customStyles: window.localStorage.getItem("customStyles") || "", }); @@ -102,6 +105,10 @@ export function SettingsGeneral() { userPreferences.repackUpdatesNotificationsEnabled ?? false, achievementNotificationsEnabled: userPreferences.achievementNotificationsEnabled ?? false, + achievementCustomNotificationsEnabled: + userPreferences.achievementCustomNotificationsEnabled ?? true, + achievementCustomNotificationPosition: + userPreferences.achievementCustomNotificationPosition ?? "top_left", friendRequestNotificationsEnabled: userPreferences.friendRequestNotificationsEnabled ?? false, language: language ?? "en", @@ -109,6 +116,21 @@ export function SettingsGeneral() { } }, [userPreferences, defaultDownloadsPath]); + const achievementCustomNotificationPositionOptions = useMemo(() => { + return [ + "top_left", + "top_right", + "bottom_left", + "bottom_right", + "top_center", + "bottom_center", + ].map((position) => ({ + key: position, + value: position, + label: t(position), + })); + }, [t]); + const handleLanguageChange = ( event: React.ChangeEvent ) => { @@ -118,9 +140,19 @@ export function SettingsGeneral() { changeLanguage(value); }; - const handleChange = (values: Partial) => { + const handleChange = async (values: Partial) => { setForm((prev) => ({ ...prev, ...values })); - updateUserPreferences(values); + await updateUserPreferences(values); + }; + + const handleChangeAchievementCustomNotificationPosition = async ( + event: React.ChangeEvent + ) => { + const value = event.target.value as AchievementCustomNotificationPosition; + + await handleChange({ achievementCustomNotificationPosition: value }); + + window.electron.updateAchievementCustomNotificationWindowPosition(); }; const handleChooseDownloadsPath = async () => { @@ -205,6 +237,17 @@ export function SettingsGeneral() { } /> + + handleChange({ + friendRequestNotificationsEnabled: + !form.friendRequestNotificationsEnabled, + }) + } + /> + handleChange({ - friendRequestNotificationsEnabled: - !form.friendRequestNotificationsEnabled, + achievementCustomNotificationsEnabled: + !form.achievementCustomNotificationsEnabled, }) } /> + {form.achievementNotificationsEnabled && + form.achievementCustomNotificationsEnabled && ( + + )} +

{t("common_redist")}

diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 764998af..bdbb72a7 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -70,6 +70,14 @@ export interface GameAchievement { cacheExpiresTimestamp: number | undefined; } +export type AchievementCustomNotificationPosition = + | "top_left" + | "top_center" + | "top_right" + | "bottom_left" + | "bottom_center" + | "bottom_right"; + export interface UserPreferences { downloadsPath?: string | null; language?: string; @@ -86,6 +94,8 @@ export interface UserPreferences { downloadNotificationsEnabled?: boolean; repackUpdatesNotificationsEnabled?: boolean; achievementNotificationsEnabled?: boolean; + achievementCustomNotificationsEnabled?: boolean; + achievementCustomNotificationPosition?: AchievementCustomNotificationPosition; friendRequestNotificationsEnabled?: boolean; showDownloadSpeedInMegabytes?: boolean; extractFilesByDefault?: boolean;