mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
feat: shadow dom to isolate achievements window and custom css refactor
This commit is contained in:
@@ -69,6 +69,7 @@
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-redux": "^9.1.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-shadow": "^20.6.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"sound-play": "^1.1.0",
|
||||
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -141,7 +141,7 @@ $margin-bottom: 28px;
|
||||
|
||||
.achievement-notification {
|
||||
width: 360px;
|
||||
height: 192px;
|
||||
height: 140px;
|
||||
display: flex;
|
||||
|
||||
&--top-left {
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SettingsAppearance({
|
||||
}, [loadThemes]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onCssInjected(() => {
|
||||
const unsubscribe = window.electron.onCustomThemeUpdated(() => {
|
||||
loadThemes();
|
||||
});
|
||||
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -6069,6 +6069,11 @@ humanize-ms@^1.2.1:
|
||||
dependencies:
|
||||
ms "^2.0.0"
|
||||
|
||||
humps@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa"
|
||||
integrity sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==
|
||||
|
||||
husky@^9.1.7:
|
||||
version "9.1.7"
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d"
|
||||
@@ -7863,6 +7868,13 @@ react-router@6.26.2:
|
||||
dependencies:
|
||||
"@remix-run/router" "1.19.2"
|
||||
|
||||
react-shadow@^20.6.0:
|
||||
version "20.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-shadow/-/react-shadow-20.6.0.tgz#13c11ec50787ba6ab637381814562066be368d49"
|
||||
integrity sha512-kY+w4OMNZ8Nj9YI9eiTgvvJ/wYO7XyX1D/LYhvwQZv5vw69iCiDtGB0BX/2U8gLUuZAMN+x/7rHJKqHh8wXFHQ==
|
||||
dependencies:
|
||||
humps "^2.0.1"
|
||||
|
||||
react-style-singleton@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
||||
|
||||
Reference in New Issue
Block a user