feat: achievement notification custom position and animations

This commit is contained in:
Zamitto
2025-05-15 19:42:23 -03:00
parent 42e8a68c08
commit 6f43da8d28
13 changed files with 502 additions and 152 deletions

View File

@@ -87,7 +87,7 @@ import "./cloud-save/upload-save-game";
import "./cloud-save/delete-game-artifact"; import "./cloud-save/delete-game-artifact";
import "./cloud-save/select-game-backup-path"; import "./cloud-save/select-game-backup-path";
import "./notifications/publish-new-repacks-notification"; import "./notifications/publish-new-repacks-notification";
import "./notifications/update-achievement-notification-window-position"; import "./notifications/update-achievement-notification-window";
import "./themes/add-custom-theme"; import "./themes/add-custom-theme";
import "./themes/delete-custom-theme"; import "./themes/delete-custom-theme";
import "./themes/get-all-custom-themes"; import "./themes/get-all-custom-themes";

View File

@@ -1,19 +0,0 @@
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

@@ -0,0 +1,29 @@
import { db, levelKeys } from "@main/level";
import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services";
import { UserPreferences } from "@types";
const updateAchievementCustomNotificationWindow = async (
_event: Electron.IpcMainInvokeEvent
) => {
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
WindowManager.closeNotificationWindow();
if (
userPreferences.achievementNotificationsEnabled &&
userPreferences.achievementCustomNotificationsEnabled !== false
) {
WindowManager.createNotificationWindow(true);
}
};
registerEvent(
"updateAchievementCustomNotificationWindow",
updateAchievementCustomNotificationWindow
);

View File

@@ -7,11 +7,16 @@ import {
findAllAchievementFiles, findAllAchievementFiles,
getAlternativeObjectIds, getAlternativeObjectIds,
} from "./find-achivement-files"; } from "./find-achivement-files";
import type { AchievementFile, Game, UnlockedAchievement } from "@types"; import type {
AchievementFile,
Game,
UnlockedAchievement,
UserPreferences,
} from "@types";
import { achievementsLogger } from "../logger"; import { achievementsLogger } from "../logger";
import { Cracker } from "@shared"; import { Cracker } from "@shared";
import { publishCombinedNewAchievementNotification } from "../notifications"; import { publishCombinedNewAchievementNotification } from "../notifications";
import { gamesSublevel } from "@main/level"; import { db, gamesSublevel, levelKeys } from "@main/level";
import { WindowManager } from "../window-manager"; import { WindowManager } from "../window-manager";
import { sleep } from "@main/helpers"; import { sleep } from "@main/helpers";
@@ -186,7 +191,7 @@ export class AchievementWatcherManager {
return mergeAchievements(game, unlockedAchievements, false); return mergeAchievements(game, unlockedAchievements, false);
} }
private static preSearchAchievementsWindows = async () => { private static async getGameAchievementFilesWindows() {
const games = await gamesSublevel const games = await gamesSublevel
.values() .values()
.all() .all()
@@ -196,24 +201,24 @@ export class AchievementWatcherManager {
return Promise.all( return Promise.all(
games.map((game) => { games.map((game) => {
const gameAchievementFiles: AchievementFile[] = []; const achievementFiles: AchievementFile[] = [];
for (const objectId of getAlternativeObjectIds(game.objectId)) { for (const objectId of getAlternativeObjectIds(game.objectId)) {
gameAchievementFiles.push( achievementFiles.push(
...(gameAchievementFilesMap.get(objectId) || []) ...(gameAchievementFilesMap.get(objectId) || [])
); );
gameAchievementFiles.push( achievementFiles.push(
...findAchievementFileInExecutableDirectory(game) ...findAchievementFileInExecutableDirectory(game)
); );
} }
return this.preProcessGameAchievementFiles(game, gameAchievementFiles); return { game, achievementFiles };
}) })
); );
}; }
private static preSearchAchievementsWithWine = async () => { private static async getGameAchievementFilesLinux() {
const games = await gamesSublevel const games = await gamesSublevel
.values() .values()
.all() .all()
@@ -221,45 +226,70 @@ export class AchievementWatcherManager {
return Promise.all( return Promise.all(
games.map((game) => { games.map((game) => {
const gameAchievementFiles = findAchievementFiles(game); const achievementFiles = findAchievementFiles(game);
const achievementFileInsideDirectory = const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game); findAchievementFileInExecutableDirectory(game);
gameAchievementFiles.push(...achievementFileInsideDirectory); achievementFiles.push(...achievementFileInsideDirectory);
return this.preProcessGameAchievementFiles(game, gameAchievementFiles); return { game, achievementFiles };
}) })
); );
}; }
public static async preSearchAchievements() { public static async preSearchAchievements() {
await sleep(5000); await sleep(2000);
try { try {
const newAchievementsCount = const gameAchievementFiles =
process.platform === "win32" process.platform === "win32"
? await this.preSearchAchievementsWindows() ? await this.getGameAchievementFilesWindows()
: await this.preSearchAchievementsWithWine(); : await this.getGameAchievementFilesLinux();
const newAchievementsCount: number[] = [];
for (const { game, achievementFiles } of gameAchievementFiles) {
const result = await this.preProcessGameAchievementFiles(
game,
achievementFiles
);
newAchievementsCount.push(result);
}
const totalNewGamesWithAchievements = newAchievementsCount.filter( const totalNewGamesWithAchievements = newAchievementsCount.filter(
(achievements) => achievements (achievements) => achievements
).length; ).length;
const totalNewAchievements = newAchievementsCount.reduce( const totalNewAchievements = newAchievementsCount.reduce(
(acc, val) => acc + val, (acc, val) => acc + val,
0 0
); );
if (totalNewAchievements > 0) { if (totalNewAchievements > 0) {
WindowManager.notificationWindow?.webContents.send( const userPreferences = await db.get<string, UserPreferences>(
"on-combined-achievements-unlocked", levelKeys.userPreferences,
totalNewGamesWithAchievements, {
totalNewAchievements valueEncoding: "json",
}
); );
publishCombinedNewAchievementNotification( if (userPreferences.achievementNotificationsEnabled) {
totalNewAchievements, if (userPreferences.achievementCustomNotificationsEnabled !== false) {
totalNewGamesWithAchievements WindowManager.notificationWindow?.webContents.send(
); "on-combined-achievements-unlocked",
totalNewGamesWithAchievements,
totalNewAchievements,
userPreferences.achievementCustomNotificationPosition ??
"top_left"
);
} else {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
);
}
}
} }
} catch (err) { } catch (err) {
achievementsLogger.error("Error on preSearchAchievements", err); achievementsLogger.error("Error on preSearchAchievements", err);

View File

@@ -106,20 +106,23 @@ export const mergeAchievements = async (
}; };
}); });
WindowManager.notificationWindow?.webContents.send( if (userPreferences?.achievementCustomNotificationsEnabled !== false) {
"on-achievement-unlocked", WindowManager.notificationWindow?.webContents.send(
game.objectId, "on-achievement-unlocked",
game.shop, game.objectId,
achievementsInfo game.shop,
); userPreferences.achievementCustomNotificationPosition ?? "top_left",
achievementsInfo
publishNewAchievementNotification({ );
achievements: achievementsInfo, } else {
unlockedAchievementCount: mergedLocalAchievements.length, publishNewAchievementNotification({
totalAchievementCount: achievementsData.length, achievements: achievementsInfo,
gameTitle: game.title, unlockedAchievementCount: mergedLocalAchievements.length,
gameIcon: game.iconUrl, totalAchievementCount: achievementsData.length,
}); gameTitle: game.title,
gameIcon: game.iconUrl,
});
}
} }
if (game.remoteId) { if (game.remoteId) {

View File

@@ -18,7 +18,11 @@ import { HydraApi } from "./hydra-api";
import UserAgent from "user-agents"; import UserAgent from "user-agents";
import { db, gamesSublevel, levelKeys } from "@main/level"; import { db, gamesSublevel, levelKeys } from "@main/level";
import { orderBy, slice } from "lodash-es"; import { orderBy, slice } from "lodash-es";
import type { ScreenState, UserPreferences } from "@types"; import type {
AchievementCustomNotificationPosition,
ScreenState,
UserPreferences,
} from "@types";
import { AuthPage } from "@shared"; import { AuthPage } from "@shared";
import { isStaging } from "@main/constants"; import { isStaging } from "@main/constants";
@@ -276,59 +280,44 @@ export class WindowManager {
} }
} }
private static readonly NOTIFICATION_WINDOW_WIDTH = 350; private static readonly NOTIFICATION_WINDOW_WIDTH = 360;
private static readonly NOTIFICATION_WINDOW_HEIGHT = 104; private static readonly NOTIFICATION_WINDOW_HEIGHT = 140;
public static async getNotificationWindowPosition() {
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
private static async getNotificationWindowPosition(
position: AchievementCustomNotificationPosition | undefined
) {
const display = screen.getPrimaryDisplay(); const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize; const { width, height } = display.workAreaSize;
if ( if (position === "bottom_center") {
userPreferences?.achievementCustomNotificationPosition === "bottom_center"
) {
return { return {
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2, x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: height - this.NOTIFICATION_WINDOW_HEIGHT, y: height - this.NOTIFICATION_WINDOW_HEIGHT,
}; };
} }
if ( if (position === "bottom_right") {
userPreferences?.achievementCustomNotificationPosition === "bottom_right"
) {
return { return {
x: width - this.NOTIFICATION_WINDOW_WIDTH, x: width - this.NOTIFICATION_WINDOW_WIDTH,
y: height - this.NOTIFICATION_WINDOW_HEIGHT, y: height - this.NOTIFICATION_WINDOW_HEIGHT,
}; };
} }
if ( if (position === "top_center") {
userPreferences?.achievementCustomNotificationPosition === "top_center"
) {
return { return {
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2, x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: 0, y: 0,
}; };
} }
if ( if (position === "bottom_left") {
userPreferences?.achievementCustomNotificationPosition === "bottom_left"
) {
return { return {
x: 0, x: 0,
y: height - this.NOTIFICATION_WINDOW_HEIGHT, y: height - this.NOTIFICATION_WINDOW_HEIGHT,
}; };
} }
if ( if (position === "top_right") {
userPreferences?.achievementCustomNotificationPosition === "top_right"
) {
return { return {
x: width - this.NOTIFICATION_WINDOW_WIDTH, x: width - this.NOTIFICATION_WINDOW_WIDTH,
y: 0, y: 0,
@@ -341,8 +330,16 @@ export class WindowManager {
}; };
} }
public static async createNotificationWindow() { public static async createNotificationWindow(showTestNotification = false) {
const { x, y } = await this.getNotificationWindowPosition(); const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
const { x, y } = await this.getNotificationWindowPosition(
userPreferences.achievementCustomNotificationPosition
);
this.notificationWindow = new BrowserWindow({ this.notificationWindow = new BrowserWindow({
transparent: true, transparent: true,
@@ -372,9 +369,25 @@ export class WindowManager {
if (isStaging) { if (isStaging) {
this.notificationWindow?.webContents.openDevTools(); this.notificationWindow?.webContents.openDevTools();
} }
if (showTestNotification) {
setTimeout(() => {
this.notificationWindow?.webContents.send(
"on-test-achievement-notification",
userPreferences.achievementCustomNotificationPosition ?? "top_left"
);
}, 1000);
}
}); });
} }
public static async closeNotificationWindow() {
if (this.notificationWindow) {
this.notificationWindow.close();
this.notificationWindow = null;
}
}
public static openEditorWindow(themeId: string) { public static openEditorWindow(themeId: string) {
if (this.mainWindow) { if (this.mainWindow) {
const existingWindow = this.editorWindows.get(themeId); const existingWindow = this.editorWindows.get(themeId);

View File

@@ -18,6 +18,7 @@ import type {
FriendRequestSync, FriendRequestSync,
ShortcutLocation, ShortcutLocation,
ShopAssets, ShopAssets,
AchievementCustomNotificationPosition,
} from "@types"; } from "@types";
import type { AuthPage, CatalogueCategory } from "@shared"; import type { AuthPage, CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
@@ -410,39 +411,63 @@ contextBridge.exposeInMainWorld("electron", {
cb: ( cb: (
objectId: string, objectId: string,
shop: GameShop, shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[] position: AchievementCustomNotificationPosition,
achievements?: {
displayName: string;
iconUrl: string;
isHidden: boolean;
isRare: boolean;
isPlatinum: boolean;
}[]
) => void ) => void
) => { ) => {
const listener = ( const listener = (
_event: Electron.IpcRendererEvent, _event: Electron.IpcRendererEvent,
objectId: string, objectId: string,
shop: GameShop, shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[] position: AchievementCustomNotificationPosition,
) => cb(objectId, shop, achievements); achievements?: {
displayName: string;
iconUrl: string;
isHidden: boolean;
isRare: boolean;
isPlatinum: boolean;
}[]
) => cb(objectId, shop, position, achievements);
ipcRenderer.on("on-achievement-unlocked", listener); ipcRenderer.on("on-achievement-unlocked", listener);
return () => return () =>
ipcRenderer.removeListener("on-achievement-unlocked", listener); ipcRenderer.removeListener("on-achievement-unlocked", listener);
}, },
onCombinedAchievementsUnlocked: ( onCombinedAchievementsUnlocked: (
cb: (gameCount: number, achievementsCount: number) => void cb: (
gameCount: number,
achievementsCount: number,
position: AchievementCustomNotificationPosition
) => void
) => { ) => {
const listener = ( const listener = (
_event: Electron.IpcRendererEvent, _event: Electron.IpcRendererEvent,
gameCount: number, gameCount: number,
achievementCount: number achievementCount: number,
) => cb(gameCount, achievementCount); position: AchievementCustomNotificationPosition
) => cb(gameCount, achievementCount, position);
ipcRenderer.on("on-combined-achievements-unlocked", listener); ipcRenderer.on("on-combined-achievements-unlocked", listener);
return () => return () =>
ipcRenderer.removeListener("on-combined-achievements-unlocked", listener); ipcRenderer.removeListener("on-combined-achievements-unlocked", listener);
}, },
onTestAchievementNotification: (cb: () => void) => { onTestAchievementNotification: (
const listener = (_event: Electron.IpcRendererEvent) => cb(); cb: (position: AchievementCustomNotificationPosition) => void
) => {
const listener = (
_event: Electron.IpcRendererEvent,
position: AchievementCustomNotificationPosition
) => cb(position);
ipcRenderer.on("on-test-achievement-notification", listener); ipcRenderer.on("on-test-achievement-notification", listener);
return () => return () =>
ipcRenderer.removeListener("on-test-achievement-notification", listener); ipcRenderer.removeListener("on-test-achievement-notification", listener);
}, },
updateAchievementCustomNotificationWindowPosition: () => updateAchievementCustomNotificationWindow: () =>
ipcRenderer.invoke("updateAchievementCustomNotificationWindowPosition"), ipcRenderer.invoke("updateAchievementCustomNotificationWindow"),
/* Themes */ /* Themes */
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme), addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -35,6 +35,7 @@ import type {
CatalogueSearchResult, CatalogueSearchResult,
ShopAssets, ShopAssets,
ShopDetailsWithAssets, ShopDetailsWithAssets,
AchievementCustomNotificationPosition,
} from "@types"; } from "@types";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import type disk from "diskusage"; import type disk from "diskusage";
@@ -326,14 +327,27 @@ declare global {
cb: ( cb: (
objectId: string, objectId: string,
shop: GameShop, shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[] position: AchievementCustomNotificationPosition,
achievements?: {
displayName: string;
iconUrl: string;
isHidden: boolean;
isRare: boolean;
isPlatinum: boolean;
}[]
) => void ) => void
) => () => Electron.IpcRenderer; ) => () => Electron.IpcRenderer;
onCombinedAchievementsUnlocked: ( onCombinedAchievementsUnlocked: (
cb: (gameCount: number, achievementCount: number) => void cb: (
gameCount: number,
achievementCount: number,
position: AchievementCustomNotificationPosition
) => void
) => () => Electron.IpcRenderer; ) => () => Electron.IpcRenderer;
onTestAchievementNotification: (cb: () => void) => Electron.IpcRenderer; onTestAchievementNotification: (
updateAchievementCustomNotificationWindowPosition: () => Promise<void>; cb: (position: AchievementCustomNotificationPosition) => void
) => Electron.IpcRenderer;
updateAchievementCustomNotificationWindow: () => Promise<void>;
/* Themes */ /* Themes */
addCustomTheme: (theme: Theme) => Promise<void>; addCustomTheme: (theme: Theme) => Promise<void>;

View File

@@ -1,35 +1,221 @@
@use "../../../scss/globals.scss"; @use "../../../scss/globals.scss";
@keyframes achievement-in { $margin-horizontal: 40px;
$margin-vertical: 52px;
@keyframes content-in {
0% { 0% {
transform: translateY(-240px); width: 80px;
opacity: 0;
transform: scale(0);
} }
100% { 100% {
transform: translateY(0); width: 80px;
opacity: 1;
transform: scale(1);
} }
} }
@keyframes achievement-out { @keyframes content-wait {
0% { 0% {
transform: translateY(0); width: 80px;
} }
100% { 100% {
transform: translateY(-240px); width: 80px;
}
}
@keyframes trophy-out {
0% {
opacity: 1;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes ellipses-stand-by {
0% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes ellipses-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
scale: 1.5;
}
}
@keyframes content-expand {
0% {
width: 80px;
}
100% {
width: calc(360px - $margin-horizontal);
}
}
@keyframes title-in {
0% {
transform: translateY(10px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes description-in {
0% {
transform: translateY(20px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes dark-overlay {
0% {
opacity: 0.7;
}
50% {
opacity: 0.7;
}
100% {
opacity: 0;
}
}
@keyframes content-out {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-20px);
opacity: 0;
} }
} }
.achievement-notification { .achievement-notification {
margin-top: 24px; position: relative;
margin-left: 24px; display: grid;
animation-duration: 1s; width: calc(360px - $margin-horizontal);
height: 60px; height: 140px;
display: flex; overflow: hidden;
animation-name: achievement-in; animation:
transform: translateY(0); content-in 450ms ease-in-out,
content-wait 450ms ease-in-out 450ms,
content-expand 450ms ease-in-out 900ms;
&::before {
content: "";
position: absolute;
top: 8px;
left: 8px;
width: 64px;
height: 64px;
opacity: 0;
z-index: 1;
background: url("/src/assets/icons/ellipses.png");
background-size: contain;
animation: ellipses-out 900ms ease-in-out;
}
&::after {
content: "";
position: absolute;
top: 0px;
width: 80px;
height: 80px;
opacity: 0;
background: url("/src/assets/icons/trophy.png") no-repeat center;
animation: trophy-out 900ms ease-in-out;
}
&.top_left {
margin: $margin-vertical 0 0 $margin-horizontal;
}
&.top_center {
margin: $margin-vertical 0 0 $margin-horizontal;
}
&.top_right {
margin: $margin-vertical $margin-horizontal 0 0;
align-self: end;
}
&.bottom_left {
margin: 0 0 $margin-vertical $margin-horizontal;
}
&.bottom_center {
margin: 0 0 $margin-vertical $margin-horizontal;
}
&.bottom_right {
margin: 0 $margin-horizontal $margin-vertical 0;
align-self: end;
}
&.closing { &.closing {
animation-name: achievement-out; transform: translateY(-20px);
transform: translateY(-240px); opacity: 0;
animation: content-out 450ms ease-in-out;
}
&__container {
width: calc(360px - $margin-horizontal);
max-width: 100%;
border: 1px solid #ffffff1a;
display: flex;
padding: 8px 16px 8px 8px;
background: globals.$background-color;
transform: translateY(0);
box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.25);
&.top_left {
align-self: flex-start;
justify-self: flex-start;
}
&.top_center {
align-self: flex-start;
justify-self: center;
}
&.top_right {
align-self: flex-start;
justify-self: flex-end;
}
&.bottom_left {
align-self: flex-end;
justify-self: flex-start;
}
&.bottom_center {
align-self: flex-end;
justify-self: center;
}
&.bottom_right {
align-self: flex-end;
justify-self: flex-end;
}
} }
&__content { &__content {
@@ -37,7 +223,51 @@
flex-direction: row; flex-direction: row;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
background: globals.$background-color; width: 100%;
padding-right: 8px; overflow: hidden;
position: relative;
&::after {
content: "";
position: absolute;
width: 64px;
height: 64px;
background: #000;
opacity: 0;
animation: dark-overlay 900ms ease-in-out;
}
}
&__icon {
min-width: 64px;
min-height: 64px;
border-radius: 2px;
flex: 1;
position: relative;
}
&__text-container {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
&__title {
font-size: 14px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
color: globals.$muted-color;
animation: title-in 450ms ease-in-out 900ms;
}
&__description {
font-size: 14px;
font-weight: 400;
white-space: nowrap;
overflow: hidden;
color: globals.$body-color;
animation: description-in 450ms ease-in-out 900ms;
} }
} }

View File

@@ -3,6 +3,7 @@ import achievementSound from "@renderer/assets/audio/achievement.wav";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import cn from "classnames"; import cn from "classnames";
import "./achievement-notification.scss"; import "./achievement-notification.scss";
import { AchievementCustomNotificationPosition } from "@types";
interface AchievementInfo { interface AchievementInfo {
displayName: string; displayName: string;
@@ -16,6 +17,8 @@ export function AchievementNotification() {
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [position, setPosition] =
useState<AchievementCustomNotificationPosition>("top_left");
const [achievements, setAchievements] = useState<AchievementInfo[]>([]); const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
const [currentAchievement, setCurrentAchievement] = const [currentAchievement, setCurrentAchievement] =
@@ -33,9 +36,11 @@ export function AchievementNotification() {
useEffect(() => { useEffect(() => {
const unsubscribe = window.electron.onCombinedAchievementsUnlocked( const unsubscribe = window.electron.onCombinedAchievementsUnlocked(
(gameCount, achievementCount) => { (gameCount, achievementCount, position) => {
if (gameCount === 0 || achievementCount === 0) return; if (gameCount === 0 || achievementCount === 0) return;
setPosition(position);
setAchievements([ setAchievements([
{ {
displayName: t("new_achievements_unlocked", { displayName: t("new_achievements_unlocked", {
@@ -58,9 +63,10 @@ export function AchievementNotification() {
useEffect(() => { useEffect(() => {
const unsubscribe = window.electron.onAchievementUnlocked( const unsubscribe = window.electron.onAchievementUnlocked(
(_object, _shop, achievements) => { (_object, _shop, position, achievements) => {
if (!achievements?.length) return; if (!achievements?.length) return;
setPosition(position);
setAchievements((ach) => ach.concat(achievements)); setAchievements((ach) => ach.concat(achievements));
playAudio(); playAudio();
@@ -73,19 +79,22 @@ export function AchievementNotification() {
}, [playAudio]); }, [playAudio]);
useEffect(() => { useEffect(() => {
const unsubscribe = window.electron.onTestAchievementNotification(() => { const unsubscribe = window.electron.onTestAchievementNotification(
setAchievements((ach) => (position) => {
ach.concat([ setPosition(position);
{ setAchievements((ach) =>
displayName: "Test Achievement", ach.concat([
iconUrl: {
"https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fc.tenor.com%2FRwKr7hVnXREAAAAC%2Fnyan-cat.gif&f=1&nofb=1&ipt=706fd8b00cbfb5b2d2621603834d5f32c0f34cce7113de228d2fcc2247a80318", 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(); playAudio();
}); }
);
return () => { return () => {
unsubscribe(); unsubscribe();
@@ -104,7 +113,7 @@ export function AchievementNotification() {
const zero = performance.now(); const zero = performance.now();
closingAnimation.current = requestAnimationFrame( closingAnimation.current = requestAnimationFrame(
function animateClosing(time) { function animateClosing(time) {
if (time - zero <= 1000) { if (time - zero <= 450) {
closingAnimation.current = requestAnimationFrame(animateClosing); closingAnimation.current = requestAnimationFrame(animateClosing);
} else { } else {
setIsVisible(false); setIsVisible(false);
@@ -147,18 +156,30 @@ export function AchievementNotification() {
return ( return (
<div <div
className={cn("achievement-notification", { className={cn("achievement-notification", {
[position]: true,
closing: isClosing, closing: isClosing,
})} })}
> >
<div className="achievement-notification__content"> <div
<img className={cn("achievement-notification__container", {
src={currentAchievement.iconUrl} closing: isClosing,
alt={currentAchievement.displayName} [position]: true,
style={{ flex: 1, width: "60px" }} })}
/> >
<div> <div className="achievement-notification__content">
<p>{t("achievement_unlocked")}</p> <img
<p>{currentAchievement.displayName}</p> src={currentAchievement.iconUrl}
alt={currentAchievement.displayName}
className="achievement-notification__icon"
/>
<div className="achievement-notification__text-container">
<p className="achievement-notification__title">
{t("achievement_unlocked")}
</p>
<p className="achievement-notification__description">
{currentAchievement.displayName}
</p>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -119,10 +119,10 @@ export function SettingsGeneral() {
const achievementCustomNotificationPositionOptions = useMemo(() => { const achievementCustomNotificationPositionOptions = useMemo(() => {
return [ return [
"top_left", "top_left",
"top_center",
"top_right", "top_right",
"bottom_left", "bottom_left",
"bottom_right", "bottom_right",
"top_center",
"bottom_center", "bottom_center",
].map((position) => ({ ].map((position) => ({
key: position, key: position,
@@ -152,7 +152,7 @@ export function SettingsGeneral() {
await handleChange({ achievementCustomNotificationPosition: value }); await handleChange({ achievementCustomNotificationPosition: value });
window.electron.updateAchievementCustomNotificationWindowPosition(); window.electron.updateAchievementCustomNotificationWindow();
}; };
const handleChooseDownloadsPath = async () => { const handleChooseDownloadsPath = async () => {
@@ -251,24 +251,28 @@ export function SettingsGeneral() {
<CheckboxField <CheckboxField
label={t("enable_achievement_notifications")} label={t("enable_achievement_notifications")}
checked={form.achievementNotificationsEnabled} checked={form.achievementNotificationsEnabled}
onChange={() => onChange={async () => {
handleChange({ await handleChange({
achievementNotificationsEnabled: achievementNotificationsEnabled:
!form.achievementNotificationsEnabled, !form.achievementNotificationsEnabled,
}) });
}
window.electron.updateAchievementCustomNotificationWindow();
}}
/> />
<CheckboxField <CheckboxField
label={t("enable_achievement_custom_notifications")} label={t("enable_achievement_custom_notifications")}
checked={form.achievementCustomNotificationsEnabled} checked={form.achievementCustomNotificationsEnabled}
disabled={!form.achievementNotificationsEnabled} disabled={!form.achievementNotificationsEnabled}
onChange={() => onChange={async () => {
handleChange({ await handleChange({
achievementCustomNotificationsEnabled: achievementCustomNotificationsEnabled:
!form.achievementCustomNotificationsEnabled, !form.achievementCustomNotificationsEnabled,
}) });
}
window.electron.updateAchievementCustomNotificationWindow();
}}
/> />
{form.achievementNotificationsEnabled && {form.achievementNotificationsEnabled &&