feat: shadow dom to isolate achievements window and custom css refactor

This commit is contained in:
Zamitto
2025-05-17 23:48:30 -03:00
parent 4485f62946
commit 0b83554565
14 changed files with 118 additions and 71 deletions

View File

@@ -1,5 +1,6 @@
import { themesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services";
const toggleCustomTheme = async (
_event: Electron.IpcMainInvokeEvent,
@@ -17,6 +18,8 @@ const toggleCustomTheme = async (
isActive,
updatedAt: new Date(),
});
WindowManager.notificationWindow?.webContents.send("on-custom-theme-updated");
};
registerEvent("toggleCustomTheme", toggleCustomTheme);

View File

@@ -20,8 +20,10 @@ const updateCustomTheme = async (
});
if (theme.isActive) {
WindowManager.mainWindow?.webContents.send("css-injected", code);
WindowManager.notificationWindow?.webContents.send("css-injected", code);
WindowManager.mainWindow?.webContents.send("on-custom-theme-updated");
WindowManager.notificationWindow?.webContents.send(
"on-custom-theme-updated"
);
}
};

View File

@@ -29,7 +29,7 @@ export class HydraApi {
private static instance: AxiosInstance;
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
private static readonly ADD_LOG_INTERCEPTOR = true;
private static readonly ADD_LOG_INTERCEPTOR = false;
private static secondsToMilliseconds(seconds: number) {
return seconds * 1000;

View File

@@ -289,6 +289,21 @@ export class WindowManager {
const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize;
console.log(
width,
height,
this.NOTIFICATION_WINDOW_HEIGHT,
this.NOTIFICATION_WINDOW_WIDTH,
position
);
if (position === "bottom-left") {
return {
x: 0,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "bottom-center") {
return {
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
@@ -310,13 +325,6 @@ export class WindowManager {
};
}
if (position === "bottom-left") {
return {
x: 0,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "top-right") {
return {
x: width - this.NOTIFICATION_WINDOW_WIDTH,
@@ -372,8 +380,13 @@ export class WindowManager {
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
// visibleOnFullScreen: true,
// });
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
this.loadNotificationWindowURL();
if (isStaging) {
this.notificationWindow.webContents.openDevTools();
}
}
public static async showAchievementTestNotification() {

View File

@@ -462,11 +462,11 @@ contextBridge.exposeInMainWorld("electron", {
/* Editor */
openEditorWindow: (themeId: string) =>
ipcRenderer.invoke("openEditorWindow", themeId),
onCssInjected: (cb: (cssString: string) => void) => {
const listener = (_event: Electron.IpcRendererEvent, cssString: string) =>
cb(cssString);
ipcRenderer.on("css-injected", listener);
return () => ipcRenderer.removeListener("css-injected", listener);
onCustomThemeUpdated: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-custom-theme-updated", listener);
return () =>
ipcRenderer.removeListener("on-custom-theme-updated", listener);
},
closeEditorWindow: (themeId?: string) =>
ipcRenderer.invoke("closeEditorWindow", themeId),

View File

@@ -28,7 +28,7 @@ import { downloadSourcesTable } from "./dexie";
import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import { injectCustomCss } from "./helpers";
import { injectCustomCss, removeCustomCss } from "./helpers";
import "./app.scss";
export interface AppProps {
@@ -246,17 +246,27 @@ export function App() {
};
}, [updateRepacks]);
useEffect(() => {
const loadAndApplyTheme = async () => {
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {
injectCustomCss(activeTheme.code);
}
};
loadAndApplyTheme();
const loadAndApplyTheme = useCallback(async () => {
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {
injectCustomCss(activeTheme.code);
} else {
removeCustomCss();
}
}, []);
useEffect(() => {
loadAndApplyTheme();
}, [loadAndApplyTheme]);
useEffect(() => {
const unsubscribe = window.electron.onCustomThemeUpdated(() => {
loadAndApplyTheme();
});
return () => unsubscribe();
}, [loadAndApplyTheme]);
const playAudio = useCallback(() => {
const audio = new Audio(achievementSound);
audio.volume = 0.2;
@@ -273,14 +283,6 @@ export function App() {
};
}, [playAudio]);
useEffect(() => {
const unsubscribe = window.electron.onCssInjected((cssString) => {
injectCustomCss(cssString);
});
return () => unsubscribe();
}, []);
const handleToastClose = useCallback(() => {
dispatch(closeToast());
}, [dispatch]);

View File

@@ -141,7 +141,7 @@ $margin-bottom: 28px;
.achievement-notification {
width: 360px;
height: 192px;
height: 140px;
display: flex;
&--top-left {

View File

@@ -3,10 +3,10 @@ import {
AchievementNotificationInfo,
} from "@types";
import cn from "classnames";
import "./achievement-notification.scss";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { EyeClosedIcon } from "@primer/octicons-react";
import Ellipses from "@renderer/assets/icons/ellipses.png";
import "./achievement-notification.scss";
interface AchievementNotificationProps {
position: AchievementCustomNotificationPosition;

View File

@@ -352,9 +352,7 @@ declare global {
/* Editor */
openEditorWindow: (themeId: string) => Promise<void>;
onCssInjected: (
cb: (cssString: string) => void
) => () => Electron.IpcRenderer;
onCustomThemeUpdated: (cb: () => void) => () => Electron.IpcRenderer;
closeEditorWindow: (themeId?: string) => Promise<void>;
}

View File

@@ -55,35 +55,32 @@ export const buildGameAchievementPath = (
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
new Color(color).darken(amount).alpha(alpha).toString();
export const injectCustomCss = (css: string) => {
export const injectCustomCss = (
css: string,
target: HTMLElement = document.head
) => {
try {
const currentCustomCss = document.getElementById("custom-css");
if (currentCustomCss) {
currentCustomCss.remove();
}
target.querySelector("#custom-css")?.remove();
if (css.startsWith(THEME_WEB_STORE_URL)) {
const link = document.createElement("link");
link.id = "custom-css";
link.rel = "stylesheet";
link.href = css;
document.head.appendChild(link);
target.appendChild(link);
} else {
const style = document.createElement("style");
style.id = "custom-css";
style.textContent = `
${css}
`;
document.head.appendChild(style);
target.appendChild(style);
}
} catch (error) {
console.error("failed to inject custom css:", error);
}
};
export const removeCustomCss = () => {
const currentCustomCss = document.getElementById("custom-css");
if (currentCustomCss) {
currentCustomCss.remove();
}
export const removeCustomCss = (target: HTMLElement = document.head) => {
target.querySelector("#custom-css")?.remove();
};

View File

@@ -5,8 +5,11 @@ import {
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
} from "@types";
import { injectCustomCss } from "@renderer/helpers";
import { injectCustomCss, removeCustomCss } from "@renderer/helpers";
import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification";
import app from "../../../app.scss?inline";
import styles from "../../../components/achievements/notification/achievement-notification.scss?inline";
import root from "react-shadow";
const NOTIFICATION_TIMEOUT = 4000;
@@ -28,6 +31,8 @@ export function AchievementNotification() {
const closingAnimation = useRef(-1);
const visibleAnimation = useRef(-1);
const [shadowRootRef, setShadowRootRef] = useState<HTMLElement | null>(null);
const playAudio = useCallback(() => {
const audio = new Audio(achievementSound);
audio.volume = 0.1;
@@ -132,31 +137,45 @@ export function AchievementNotification() {
}
}, [achievements]);
useEffect(() => {
const loadAndApplyTheme = async () => {
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {
injectCustomCss(activeTheme.code);
}
};
loadAndApplyTheme();
}, []);
const loadAndApplyTheme = useCallback(async () => {
if (!shadowRootRef) return;
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {
console.log("injecting custom css");
injectCustomCss(activeTheme.code, shadowRootRef);
} else {
console.log("removing custom css");
removeCustomCss(shadowRootRef);
}
}, [shadowRootRef]);
useEffect(() => {
const unsubscribe = window.electron.onCssInjected((cssString) => {
injectCustomCss(cssString);
loadAndApplyTheme();
}, [loadAndApplyTheme]);
useEffect(() => {
const unsubscribe = window.electron.onCustomThemeUpdated(() => {
console.log("onCustomThemeUpdated");
loadAndApplyTheme();
});
return () => unsubscribe();
}, []);
if (!isVisible || !currentAchievement) return null;
}, [loadAndApplyTheme]);
return (
<AchievementNotificationItem
achievement={currentAchievement}
isClosing={isClosing}
position={position}
/>
<root.div>
<style type="text/css">
{app} {styles}
</style>
<section ref={(ref) => setShadowRootRef(ref)}>
{isVisible && currentAchievement && (
<AchievementNotificationItem
achievement={currentAchievement}
isClosing={isClosing}
position={position}
/>
)}
</section>
</root.div>
);
}

View File

@@ -40,7 +40,7 @@ export function SettingsAppearance({
}, [loadThemes]);
useEffect(() => {
const unsubscribe = window.electron.onCssInjected(() => {
const unsubscribe = window.electron.onCustomThemeUpdated(() => {
loadThemes();
});