mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
feat: notification preview on theme editor
This commit is contained in:
@@ -21,6 +21,7 @@ const updateCustomTheme = async (
|
||||
|
||||
if (theme.isActive) {
|
||||
WindowManager.mainWindow?.webContents.send("css-injected", code);
|
||||
WindowManager.notificationWindow?.webContents.send("css-injected", code);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
AchievementCustomNotificationPosition,
|
||||
AchievementNotificationInfo,
|
||||
} from "@types";
|
||||
|
||||
export enum Downloader {
|
||||
RealDebrid,
|
||||
Torrent,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user