mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-20 01:33:56 +00:00
feat: custom achievement notification position
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
22
src/renderer/src/declaration.d.ts
vendored
22
src/renderer/src/declaration.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user