feat: custom achievement notification position

This commit is contained in:
Zamitto
2025-05-14 17:42:30 -03:00
parent 96cfa8c015
commit 96385d90d8
9 changed files with 242 additions and 54 deletions

View File

@@ -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";

View File

@@ -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
);

View File

@@ -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()
);

View File

@@ -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<string, UserPreferences>(
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) {

View File

@@ -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),

View File

@@ -175,16 +175,6 @@ declare global {
minimized: boolean;
}) => Promise<void>;
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
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<void>;
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<void>;
/* Themes */
addCustomTheme: (theme: Theme) => Promise<void>;

View File

@@ -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(() => {

View File

@@ -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<HTMLSelectElement>
) => {
@@ -118,9 +140,19 @@ export function SettingsGeneral() {
changeLanguage(value);
};
const handleChange = (values: Partial<typeof form>) => {
const handleChange = async (values: Partial<typeof form>) => {
setForm((prev) => ({ ...prev, ...values }));
updateUserPreferences(values);
await updateUserPreferences(values);
};
const handleChangeAchievementCustomNotificationPosition = async (
event: React.ChangeEvent<HTMLSelectElement>
) => {
const value = event.target.value as AchievementCustomNotificationPosition;
await handleChange({ achievementCustomNotificationPosition: value });
window.electron.updateAchievementCustomNotificationWindowPosition();
};
const handleChooseDownloadsPath = async () => {
@@ -205,6 +237,17 @@ export function SettingsGeneral() {
}
/>
<CheckboxField
label={t("enable_friend_request_notifications")}
checked={form.friendRequestNotificationsEnabled}
onChange={() =>
handleChange({
friendRequestNotificationsEnabled:
!form.friendRequestNotificationsEnabled,
})
}
/>
<CheckboxField
label={t("enable_achievement_notifications")}
checked={form.achievementNotificationsEnabled}
@@ -217,16 +260,27 @@ export function SettingsGeneral() {
/>
<CheckboxField
label={t("enable_friend_request_notifications")}
checked={form.friendRequestNotificationsEnabled}
label={t("enable_achievement_custom_notifications")}
checked={form.achievementCustomNotificationsEnabled}
disabled={!form.achievementNotificationsEnabled}
onChange={() =>
handleChange({
friendRequestNotificationsEnabled:
!form.friendRequestNotificationsEnabled,
achievementCustomNotificationsEnabled:
!form.achievementCustomNotificationsEnabled,
})
}
/>
{form.achievementNotificationsEnabled &&
form.achievementCustomNotificationsEnabled && (
<SelectField
label={t("achievement_custom_notification_position")}
value={form.achievementCustomNotificationPosition}
onChange={handleChangeAchievementCustomNotificationPosition}
options={achievementCustomNotificationPositionOptions}
/>
)}
<h2 className="settings-general__section-title">{t("common_redist")}</h2>
<p className="settings-general__common-redist-description">

View File

@@ -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;