feat: notification preview on theme editor

This commit is contained in:
Zamitto
2025-05-16 16:18:19 -03:00
parent 39c073634c
commit bc06ae5c03
13 changed files with 347 additions and 100 deletions

View File

@@ -21,6 +21,7 @@ const updateCustomTheme = async (
if (theme.isActive) {
WindowManager.mainWindow?.webContents.send("css-injected", code);
WindowManager.notificationWindow?.webContents.send("css-injected", code);
}
};

View File

@@ -20,11 +20,10 @@ import { db, gamesSublevel, levelKeys } from "@main/level";
import { orderBy, slice } from "lodash-es";
import type {
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
ScreenState,
UserPreferences,
} from "@types";
import { AuthPage } from "@shared";
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
import { isStaging } from "@main/constants";
export class WindowManager {
@@ -377,23 +376,7 @@ export class WindowManager {
this.notificationWindow?.webContents.send(
"on-achievement-unlocked",
userPreferences.achievementCustomNotificationPosition ?? "top_left",
[
{
title: t("test_achievement_notification_title", {
ns: "notifications",
lng: language,
}),
description: t("test_achievement_notification_description", {
ns: "notifications",
lng: language,
}),
iconUrl: "https://cdn.losbroxas.org/favicon.svg",
points: 100,
isHidden: false,
isRare: false,
isPlatinum: false,
},
] as AchievementNotificationInfo[]
[generateAchievementCustomNotificationTest(t, language)]
);
}, 1000);
}
@@ -419,7 +402,7 @@ export class WindowManager {
}
const editorWindow = new BrowserWindow({
width: 600,
width: 720,
height: 720,
minWidth: 600,
minHeight: 540,

View File

@@ -0,0 +1,50 @@
import {
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
} from "@types";
import cn from "classnames";
import "./achievement-notification.scss";
interface AchievementNotificationProps {
position: AchievementCustomNotificationPosition;
currentAchievement: AchievementNotificationInfo;
isClosing: boolean;
}
export function AchievementNotificationItem({
position,
currentAchievement,
isClosing,
}: Readonly<AchievementNotificationProps>) {
return (
<div
className={cn("achievement-notification", {
[position]: true,
closing: isClosing,
})}
>
<div
className={cn("achievement-notification__container", {
[position]: true,
closing: isClosing,
})}
>
<div className="achievement-notification__content">
<img
src={currentAchievement.iconUrl}
alt={currentAchievement.title}
className="achievement-notification__icon"
/>
<div className="achievement-notification__text-container">
<p className="achievement-notification__title">
{currentAchievement.title}
</p>
<p className="achievement-notification__description">
{currentAchievement.description}
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
@use "../../scss/globals.scss";
.collapsed-menu {
&__button {
height: 72px;
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
display: flex;
align-items: center;
background-color: globals.$background-color;
color: globals.$muted-color;
width: 100%;
cursor: pointer;
transition: all ease 0.2s;
gap: globals.$spacing-unit;
font-size: globals.$body-font-size;
font-weight: bold;
&:hover {
background-color: rgba(255, 255, 255, 0.05);
}
&:active {
opacity: globals.$active-opacity;
}
}
&__chevron {
transition: transform ease 0.2s;
&--open {
transform: rotate(180deg);
}
}
&__content {
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0, 1, 0, 1);
position: relative;
}
}

View File

@@ -0,0 +1,52 @@
import { useEffect, useRef, useState } from "react";
import { ChevronDownIcon } from "@primer/octicons-react";
import "./collapsed-menu.scss";
export interface CollapsedMenuProps {
title: string;
children: React.ReactNode;
}
export function CollapsedMenu({
title,
children,
}: Readonly<CollapsedMenuProps>) {
const content = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(true);
const [height, setHeight] = useState(0);
useEffect(() => {
if (content.current && content.current.scrollHeight !== height) {
setHeight(isOpen ? content.current.scrollHeight : 0);
} else if (!isOpen) {
setHeight(0);
}
}, [isOpen, children, height]);
return (
<div className="collapsed-menu">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="collapsed-menu__button"
>
<ChevronDownIcon
className={`collapsed-menu__chevron ${
isOpen ? "collapsed-menu__chevron--open" : ""
}`}
/>
<span>{title}</span>
</button>
<div
ref={content}
className="collapsed-menu__content"
style={{
maxHeight: `${height}px`,
}}
>
{children}
</div>
</div>
);
}

View File

@@ -18,12 +18,13 @@ export function SelectField({
options = [{ key: "-", value: value?.toString() || "-", label: "-" }],
theme = "primary",
onChange,
}: SelectProps) {
className,
}: Readonly<SelectProps>) {
const [isFocused, setIsFocused] = useState(false);
const id = useId();
return (
<div className="select-field__container">
<div className={cn("select-field__container", className)}>
{label && (
<label htmlFor={id} className="select-field__label">
{label}

View File

@@ -1,14 +1,14 @@
import { useCallback, useEffect, useRef, useState } from "react";
import achievementSound from "@renderer/assets/audio/achievement.wav";
import { useTranslation } from "react-i18next";
import cn from "classnames";
import "./achievement-notification.scss";
import {
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
} from "@types";
import { injectCustomCss } from "@renderer/helpers";
import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification";
const NOTIFICATION_TIMEOUT = 4000;
const NOTIFICATION_TIMEOUT = 40000;
export function AchievementNotification() {
const { t } = useTranslation("achievement");
@@ -51,8 +51,7 @@ export function AchievementNotification() {
isRare: false,
isPlatinum: false,
points: 0,
iconUrl:
"https://avatars.githubusercontent.com/u/164102380?s=400&u=01a13a7b4f0c642f7e547b8e1d70440ea06fa750&v=4",
iconUrl: "https://cdn.losbroxas.org/favicon.svg",
},
]);
@@ -133,37 +132,32 @@ export function AchievementNotification() {
}
}, [achievements]);
useEffect(() => {
const loadAndApplyTheme = async () => {
const activeTheme = await window.electron.getActiveCustomTheme();
console.log("activeTheme", activeTheme);
if (activeTheme?.code) {
injectCustomCss(activeTheme.code);
}
};
loadAndApplyTheme();
}, []);
useEffect(() => {
const unsubscribe = window.electron.onCssInjected((cssString) => {
injectCustomCss(cssString);
});
return () => unsubscribe();
}, []);
if (!isVisible || !currentAchievement) return null;
return (
<div
className={cn("achievement-notification", {
[position]: true,
closing: isClosing,
})}
>
<div
className={cn("achievement-notification__container", {
[position]: true,
closing: isClosing,
})}
>
<div className="achievement-notification__content">
<img
src={currentAchievement.iconUrl}
alt={currentAchievement.title}
className="achievement-notification__icon"
/>
<div className="achievement-notification__text-container">
<p className="achievement-notification__title">
{currentAchievement.title}
</p>
<p className="achievement-notification__description">
{currentAchievement.description}
</p>
</div>
</div>
</div>
</div>
<AchievementNotificationItem
currentAchievement={currentAchievement}
isClosing={isClosing}
position={position}
/>
);
}

View File

@@ -22,35 +22,43 @@ interface FormValues {
name: string;
}
const DEFAULT_THEME_CODE = `
/*
Here you can edit CSS for your theme and apply it on Hydra.
There are a few classes already in place, you can use them to style the launcher.
const DEFAULT_THEME_CODE = `/*
Here you can edit CSS for your theme and apply it on Hydra.
There are a few classes already in place, you can use them to style the launcher.
If you want to learn more about how to run Hydra in dev mode (which will allow you to inspect the DOM and view the classes)
or how to publish your theme in the theme store, you can check the docs:
https://docs.hydralauncher.gg/
If you want to learn more about how to run Hydra in dev mode (which will allow you to inspect the DOM and view the classes)
or how to publish your theme in the theme store, you can check the docs:
https://docs.hydralauncher.gg/themes.html
Happy hacking!
*/
Happy hacking!
*/
/* Header */
.header {}
/* Header */
.header {}
/* Sidebar */
.sidebar {}
/* Sidebar */
.sidebar {}
/* Main content */
.container__content {}
/* Main content */
.container__content {}
/* Bottom panel */
.bottom-panel {}
/* Bottom panel */
.bottom-panel {}
/* Toast */
.toast {}
/* Toast */
.toast {}
/* Button */
.button {}
/* Button */
.button {}
/* Custom Achievement Notification */
.achievement-notification {}
.achievement-notification__container {}
.achievement-notification__content {}
.achievement-notification__icon {}
.achievement-notification__text-container {}
.achievement-notification__title {}
.achievement-notification__description {}
`;

View File

@@ -36,14 +36,16 @@
}
}
&__editor {
position: relative;
width: 100%;
height: 100%;
flex: 1;
}
&__footer {
background-color: globals.$dark-background-color;
padding: globals.$spacing-unit globals.$spacing-unit * 2;
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 50;
&-actions {
display: flex;
@@ -78,4 +80,15 @@
margin-bottom: 8px;
}
}
&__notification-preview {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
&__select-variation {
flex: inherit;
}
}
}

View File

@@ -1,12 +1,23 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import "./theme-editor.scss";
import Editor from "@monaco-editor/react";
import { Theme } from "@types";
import { AchievementCustomNotificationPosition, Theme } from "@types";
import { useSearchParams } from "react-router-dom";
import { Button } from "@renderer/components";
import { Button, SelectField } from "@renderer/components";
import { CheckIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import cn from "classnames";
import { injectCustomCss } from "@renderer/helpers";
import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification";
import { generateAchievementCustomNotificationTest } from "@shared";
import { CollapsedMenu } from "@renderer/components/collapsed-menu/collapsed-menu";
const notificationVariations = {
default: "default",
rare: "rare",
platinum: "platinum",
hidden: "hidden",
};
export default function ThemeEditor() {
const [searchParams] = useSearchParams();
@@ -14,9 +25,30 @@ export default function ThemeEditor() {
const [code, setCode] = useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [isClosingNotifications, setIsClosingNotifications] = useState(false);
const themeId = searchParams.get("themeId");
const { t } = useTranslation("settings");
const { t, i18n } = useTranslation("settings");
const [notificationVariation, setNotificationVariation] =
useState<keyof typeof notificationVariations>("default");
const achievementPreview = useMemo(() => {
return {
achievement: {
...generateAchievementCustomNotificationTest(t, i18n.language),
isRare: notificationVariation === "rare",
isHidden: notificationVariation === "hidden",
isPlatinum: notificationVariation === "platinum",
},
position: "top_center" as AchievementCustomNotificationPosition,
};
}, [t, i18n.language, notificationVariation]);
useEffect(() => {
window.document.title = "Hydra - Theme Editor";
}, []);
useEffect(() => {
if (themeId) {
@@ -33,12 +65,17 @@ export default function ThemeEditor() {
if (theme) {
await window.electron.updateCustomTheme(theme.id, code);
setHasUnsavedChanges(false);
setIsClosingNotifications(true);
setTimeout(() => {
injectCustomCss(code);
setIsClosingNotifications(false);
}, 450);
}
}, [code, theme]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
event.preventDefault();
handleSave();
}
@@ -71,21 +108,62 @@ export default function ThemeEditor() {
)}
</div>
<Editor
theme="vs-dark"
defaultLanguage="css"
value={code}
onChange={handleEditorChange}
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: "on",
wordWrap: "on",
automaticLayout: true,
}}
/>
<div className="theme-editor__editor">
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
>
<Editor
theme="vs-dark"
defaultLanguage="css"
value={code}
onChange={handleEditorChange}
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: "on",
wordWrap: "on",
automaticLayout: true,
}}
/>
</div>
</div>
<div className="theme-editor__footer">
<CollapsedMenu title="Notification Preview">
<div className="theme-editor__notification-preview">
<SelectField
className="theme-editor__notification-preview__select-variation"
label="Notification Variation"
options={Object.values(notificationVariations).map(
(variation) => {
return {
key: variation,
value: variation,
label: variation,
};
}
)}
onChange={(value) =>
setNotificationVariation(
value.target.value as keyof typeof notificationVariations
)
}
/>
<AchievementNotificationItem
position={achievementPreview.position}
currentAchievement={achievementPreview.achievement}
isClosing={isClosingNotifications}
/>
</div>
</CollapsedMenu>
<div className="theme-editor__footer-actions">
<Button onClick={handleSave}>
<CheckIcon />

View File

@@ -1,3 +1,8 @@
import {
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
} from "@types";
export enum Downloader {
RealDebrid,
Torrent,

View File

@@ -16,6 +16,7 @@ import {
import { charMap } from "./char-map";
import { Downloader } from "./constants";
import { format } from "date-fns";
import { AchievementNotificationInfo } from "@types";
export * from "./constants";
@@ -175,3 +176,24 @@ export const formatDate = (
if (isNaN(new Date(date).getDate())) return "N/A";
return format(date, language == "en" ? "MM-dd-yyyy" : "dd/MM/yyyy");
};
export const generateAchievementCustomNotificationTest = (
t: any,
language?: string
): AchievementNotificationInfo => {
return {
title: t("test_achievement_notification_title", {
ns: "notifications",
lng: language ?? "en",
}),
description: t("test_achievement_notification_description", {
ns: "notifications",
lng: language ?? "en",
}),
iconUrl: "https://cdn.losbroxas.org/favicon.svg",
points: 100,
isHidden: false,
isRare: false,
isPlatinum: false,
};
};