mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-26 20:31:03 +00:00
feat: achievement notification custom position and animations
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
BIN
src/renderer/src/assets/icons/ellipses.png
Normal file
BIN
src/renderer/src/assets/icons/ellipses.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/renderer/src/assets/icons/trophy.png
Normal file
BIN
src/renderer/src/assets/icons/trophy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 417 B |
22
src/renderer/src/declaration.d.ts
vendored
22
src/renderer/src/declaration.d.ts
vendored
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
Reference in New Issue
Block a user