mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
feat: add theme editor with Monaco and custom CSS injection
This commit is contained in:
parent
3e2d7a751c
commit
5a19e9fd12
@ -36,6 +36,7 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@fontsource/noto-sans": "^5.1.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
|
@ -297,7 +297,25 @@
|
||||
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||
"bill_sent_until": "Your next bill will be sent until this day",
|
||||
"no_themes": "Seems like you don't have any themes yet, but no worries, click here to create your first masterpiece."
|
||||
"no_themes": "Seems like you don't have any themes yet, but no worries, click here to create your first masterpiece.",
|
||||
"editor_tab_code": "Code",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Save",
|
||||
"web_store": "Web store",
|
||||
"clear_themes": "Clear",
|
||||
"add_theme": "Add",
|
||||
"add_theme_modal_title": "Add custom theme",
|
||||
"add_theme_modal_description": "Create a new theme to customize Hydra's appearance",
|
||||
"theme_name": "Name",
|
||||
"insert_theme_name": "Insert theme name",
|
||||
"set_theme": "Set theme",
|
||||
"unset_theme": "Unset theme",
|
||||
"delete_theme": "Delete theme",
|
||||
"edit_theme": "Edit theme",
|
||||
"delete_all_themes": "Delete all themes",
|
||||
"delete_all_themes_description": "This will delete all your custom themes",
|
||||
"delete_theme_description": "This will delete the theme {{theme}}",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
@ -78,6 +78,11 @@ import "./themes/add-custom-theme";
|
||||
import "./themes/delete-custom-theme";
|
||||
import "./themes/get-all-custom-themes";
|
||||
import "./themes/delete-all-custom-themes";
|
||||
import "./themes/update-custom-theme";
|
||||
import "./themes/open-editor-window";
|
||||
import "./themes/get-custom-theme-by-id";
|
||||
import "./themes/get-active-custom-theme";
|
||||
import "./themes/css-injector";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
|
12
src/main/events/themes/css-injector.ts
Normal file
12
src/main/events/themes/css-injector.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const injectCSS = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
cssString: string
|
||||
) => {
|
||||
WindowManager.mainWindow?.webContents.send("css-injected", cssString);
|
||||
return;
|
||||
};
|
||||
|
||||
registerEvent("injectCSS", injectCSS);
|
10
src/main/events/themes/get-active-custom-theme.ts
Normal file
10
src/main/events/themes/get-active-custom-theme.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { themes } from "@main/level/sublevels/themes";
|
||||
import { Theme } from "@types";
|
||||
|
||||
const getActiveCustomTheme = async () => {
|
||||
const allThemes = await themes.values().all();
|
||||
return allThemes.find((theme: Theme) => theme.isActive);
|
||||
};
|
||||
|
||||
registerEvent("getActiveCustomTheme", getActiveCustomTheme);
|
8
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
8
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { themes } from "@main/level/sublevels/themes";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getCustomThemeById = async (_event: Electron.IpcMainInvokeEvent, themeId: string) => {
|
||||
return await themes.get(themeId);
|
||||
};
|
||||
|
||||
registerEvent("getCustomThemeById", getCustomThemeById);
|
8
src/main/events/themes/open-editor-window.ts
Normal file
8
src/main/events/themes/open-editor-window.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { WindowManager } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const openEditorWindow = async (_event: Electron.IpcMainInvokeEvent, themeId: string) => {
|
||||
WindowManager.openEditorWindow(themeId);
|
||||
};
|
||||
|
||||
registerEvent("openEditorWindow", openEditorWindow);
|
13
src/main/events/themes/update-custom-theme.ts
Normal file
13
src/main/events/themes/update-custom-theme.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { themes } from "@main/level/sublevels/themes";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { Theme } from "@types";
|
||||
|
||||
const updateCustomTheme = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
themeId: string,
|
||||
theme: Theme
|
||||
) => {
|
||||
await themes.put(themeId, theme);
|
||||
};
|
||||
|
||||
registerEvent("updateCustomTheme", updateCustomTheme);
|
@ -194,6 +194,58 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static openEditorWindow(themeId: string) {
|
||||
if (this.mainWindow) {
|
||||
const editorWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 720,
|
||||
minWidth: 600,
|
||||
minHeight: 540,
|
||||
backgroundColor: "#1c1c1c",
|
||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
titleBarOverlay: {
|
||||
symbolColor: "#DADBE1",
|
||||
color: "#151515",
|
||||
height: 34,
|
||||
},
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
|
||||
if (!app.isPackaged) {
|
||||
editorWindow.webContents.openDevTools();
|
||||
} else {
|
||||
this.mainWindow?.webContents.openDevTools();
|
||||
}
|
||||
|
||||
editorWindow.removeMenu();
|
||||
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
editorWindow.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/editor?themeId=${themeId}`
|
||||
);
|
||||
} else {
|
||||
editorWindow.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash: "editor",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
editorWindow.once("ready-to-show", () => {
|
||||
editorWindow.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static redirect(hash: string) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.loadMainWindowURL(hash);
|
||||
|
@ -342,4 +342,19 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
deleteAllCustomThemes: () => ipcRenderer.invoke("deleteAllCustomThemes"),
|
||||
deleteCustomTheme: (themeId: string) =>
|
||||
ipcRenderer.invoke("deleteCustomTheme", themeId),
|
||||
updateCustomTheme: (themeId: string, theme: Theme) =>
|
||||
ipcRenderer.invoke("updateCustomTheme", themeId, theme),
|
||||
getCustomThemeById: (themeId: string) =>
|
||||
ipcRenderer.invoke("getCustomThemeById", themeId),
|
||||
getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"),
|
||||
|
||||
/* Editor */
|
||||
openEditorWindow: (themeId: string) => ipcRenderer.invoke("openEditorWindow", themeId),
|
||||
injectCSS: (cssString: string) =>
|
||||
ipcRenderer.invoke("injectCSS", cssString),
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
@ -28,7 +28,9 @@ 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 "./app.scss";
|
||||
import { Theme } from "@types";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
@ -233,6 +235,29 @@ export function App() {
|
||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||
}, [updateRepacks]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAndApplyTheme = async () => {
|
||||
const activeTheme: Theme = await window.electron.getActiveCustomTheme();
|
||||
|
||||
if (activeTheme.code) {
|
||||
injectCustomCss(activeTheme.code);
|
||||
}
|
||||
};
|
||||
loadAndApplyTheme();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onCssInjected((cssString) => {
|
||||
if (cssString) {
|
||||
injectCustomCss(cssString);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
dispatch(closeToast());
|
||||
}, [dispatch]);
|
||||
|
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
@ -275,6 +275,14 @@ declare global {
|
||||
getAllCustomThemes: () => Promise<Theme[]>;
|
||||
deleteAllCustomThemes: () => Promise<void>;
|
||||
deleteCustomTheme: (themeId: string) => Promise<void>;
|
||||
updateCustomTheme: (themeId: string, theme: Theme) => Promise<void>;
|
||||
getCustomThemeById: (themeId: string) => Promise<Theme | null>;
|
||||
getActiveCustomTheme: () => Promise<Theme | null>;
|
||||
|
||||
/* Editor */
|
||||
openEditorWindow: (themeId: string) => Promise<void>;
|
||||
injectCSS: (cssString: string) => Promise<void>;
|
||||
onCssInjected: (cb: (cssString: string) => void) => () => Electron.IpcRenderer;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
@ -53,3 +53,29 @@ 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) => {
|
||||
try {
|
||||
const currentCustomCss = document.getElementById("custom-css");
|
||||
if (currentCustomCss) {
|
||||
currentCustomCss.remove();
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "custom-css";
|
||||
style.type = "text/css";
|
||||
style.textContent = `
|
||||
${css}
|
||||
`;
|
||||
document.head.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();
|
||||
}
|
||||
};
|
||||
|
@ -33,6 +33,7 @@ const Profile = React.lazy(() => import("./pages/profile/profile"));
|
||||
const Achievements = React.lazy(
|
||||
() => import("./pages/achievements/achievements")
|
||||
);
|
||||
const Editor = React.lazy(() => import("./pages/editor/editor"));
|
||||
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
@ -104,6 +105,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
element={<SuspenseWrapper Component={Achievements} />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="/editor"
|
||||
element={<SuspenseWrapper Component={Editor} />}
|
||||
/>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
|
74
src/renderer/src/pages/editor/editor.scss
Normal file
74
src/renderer/src/pages/editor/editor.scss
Normal file
@ -0,0 +1,74 @@
|
||||
@use "../../scss/globals.scss" as globals;
|
||||
|
||||
.editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&__header {
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: globals.$dark-background-color;
|
||||
font-size: 8px;
|
||||
z-index: 50;
|
||||
-webkit-app-region: drag;
|
||||
gap: 8px;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__status {
|
||||
display: flex;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: globals.$muted-color;
|
||||
border-radius: 50%;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&__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;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.active {
|
||||
background-color: darken(globals.$dark-background-color, 2%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding: 16px;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: globals.$muted-color;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
131
src/renderer/src/pages/editor/editor.tsx
Normal file
131
src/renderer/src/pages/editor/editor.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import "./editor.scss";
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { Theme } from '@types';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Button } from '@renderer/components';
|
||||
import { CheckIcon, CodeIcon, ProjectRoadmapIcon } from '@primer/octicons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const EditorPage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [theme, setTheme] = useState<Theme | null>(null);
|
||||
const [code, setCode] = useState('');
|
||||
const [activeTab, setActiveTab] = useState('code');
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
const themeId = searchParams.get('themeId');
|
||||
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (themeId) {
|
||||
window.electron.getCustomThemeById(themeId).then(loadedTheme => {
|
||||
if (loadedTheme) {
|
||||
setTheme(loadedTheme);
|
||||
setCode(loadedTheme.code);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [themeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [code, theme]);
|
||||
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
if (value !== undefined) {
|
||||
setCode(value);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (theme) {
|
||||
const updatedTheme = {
|
||||
...theme,
|
||||
code: code,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
await window.electron.updateCustomTheme(theme.id, updatedTheme);
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
if (theme.isActive) {
|
||||
window.electron.injectCSS(code);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="editor">
|
||||
<div className="editor__header">
|
||||
<h1>{theme?.name}</h1>
|
||||
{hasUnsavedChanges && (
|
||||
<div className="editor__header__status">
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === 'code' && (
|
||||
<Editor
|
||||
theme="vs-dark"
|
||||
defaultLanguage="css"
|
||||
value={code}
|
||||
onChange={handleEditorChange}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
wordWrap: 'on',
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'info' && (
|
||||
<div className="editor__info">
|
||||
entao mano eu ate fiz isso aqui mas tava feio dms ai deu vergonha e removi kkkk
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="editor__footer">
|
||||
<div className="editor__footer-actions">
|
||||
<div className="editor__footer-actions__tabs">
|
||||
<Button onClick={() => handleTabChange('code')} theme='dark' className={activeTab === 'code' ? 'active' : ''}>
|
||||
<CodeIcon />
|
||||
{t('editor_tab_code')}
|
||||
</Button>
|
||||
<Button onClick={() => handleTabChange('info')} theme='dark' className={activeTab === 'info' ? 'active' : ''}>
|
||||
<ProjectRoadmapIcon />
|
||||
{t('editor_tab_info')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSave}>
|
||||
<CheckIcon />
|
||||
{t('editor_tab_save')}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorPage;
|
@ -11,7 +11,7 @@ interface ThemeActionsProps {
|
||||
}
|
||||
|
||||
export const ThemeActions = ({ onListUpdated }: ThemeActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
const [addThemeModalVisible, setAddThemeModalVisible] = useState(false);
|
||||
const [deleteAllThemesModalVisible, setDeleteAllThemesModalVisible] =
|
||||
|
@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import "./theme-card.scss";
|
||||
import { useState } from "react";
|
||||
import { DeleteThemeModal } from "../modals/delete-theme-modal";
|
||||
import { injectCustomCss, removeCustomCss } from "@renderer/helpers";
|
||||
|
||||
interface ThemeCardProps {
|
||||
theme: Theme;
|
||||
@ -13,11 +14,53 @@ interface ThemeCardProps {
|
||||
}
|
||||
|
||||
export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation('settings');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [deleteThemeModalVisible, setDeleteThemeModalVisible] = useState(false);
|
||||
|
||||
const handleSetTheme = async () => {
|
||||
try {
|
||||
const currentTheme = await window.electron.getCustomThemeById(theme.id);
|
||||
|
||||
if (!currentTheme) return;
|
||||
|
||||
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||
|
||||
if (activeTheme) {
|
||||
removeCustomCss();
|
||||
await window.electron.updateCustomTheme(activeTheme.id, {
|
||||
...activeTheme,
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
|
||||
injectCustomCss(currentTheme.code);
|
||||
await window.electron.updateCustomTheme(currentTheme.id, {
|
||||
...currentTheme,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
onListUpdated();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnsetTheme = async () => {
|
||||
try {
|
||||
removeCustomCss();
|
||||
await window.electron.updateCustomTheme(theme.id, {
|
||||
...theme,
|
||||
isActive: false
|
||||
});
|
||||
|
||||
onListUpdated();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteThemeModal
|
||||
@ -25,6 +68,7 @@ export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => {
|
||||
onClose={() => setDeleteThemeModalVisible(false)}
|
||||
onThemeDeleted={onListUpdated}
|
||||
themeId={theme.id}
|
||||
themeName={theme.name}
|
||||
/>
|
||||
|
||||
<div
|
||||
@ -48,7 +92,7 @@ export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{theme.author && theme.author && (
|
||||
{theme.authorName && (
|
||||
<p className="theme-card__author">
|
||||
{t("by")}
|
||||
|
||||
@ -56,7 +100,7 @@ export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => {
|
||||
className="theme-card__author__name"
|
||||
onClick={() => navigate(`/profile/${theme.author}`)}
|
||||
>
|
||||
{theme.author}
|
||||
{theme.authorName}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
@ -64,14 +108,22 @@ export const ThemeCard = ({ theme, onListUpdated }: ThemeCardProps) => {
|
||||
<div className="theme-card__actions">
|
||||
<div className="theme-card__actions__left">
|
||||
{theme.isActive ? (
|
||||
<Button theme="dark">{t("unset_theme ")}</Button>
|
||||
<Button onClick={handleUnsetTheme} theme="dark">
|
||||
{t("unset_theme")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button theme="outline">{t("set_theme")}</Button>
|
||||
<Button onClick={handleSetTheme} theme="outline">
|
||||
{t("set_theme")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="theme-card__actions__right">
|
||||
<Button title={t("edit_theme")} theme="outline">
|
||||
<Button
|
||||
onClick={() => window.electron.openEditorWindow(theme.id)}
|
||||
title={t("edit_theme")}
|
||||
theme="outline"
|
||||
>
|
||||
<PencilIcon />
|
||||
</Button>
|
||||
|
||||
|
@ -4,6 +4,8 @@ import { Button } from "@renderer/components/button/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import "./modals.scss";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { Theme } from "@types";
|
||||
|
||||
interface AddThemeModalProps {
|
||||
visible: boolean;
|
||||
@ -17,6 +19,7 @@ export const AddThemeModal = ({
|
||||
onThemeAdded,
|
||||
}: AddThemeModalProps) => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { userDetails } = useUserDetails();
|
||||
const [name, setName] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@ -32,15 +35,20 @@ export const AddThemeModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const theme = {
|
||||
const theme: Theme = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
isActive: false,
|
||||
author: userDetails?.id || undefined,
|
||||
authorName: userDetails?.username || undefined,
|
||||
colors: {
|
||||
accent: "#c0c1c7",
|
||||
background: "#1c1c1c",
|
||||
surface: "#151515",
|
||||
},
|
||||
code: "",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await window.electron.addCustomTheme(theme);
|
||||
@ -53,8 +61,8 @@ export const AddThemeModal = ({
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("add_theme")}
|
||||
description={t("add_theme_description")}
|
||||
title={t("add_theme_modal_title")}
|
||||
description={t("add_theme_modal_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="add-theme-modal__container">
|
||||
|
@ -8,6 +8,7 @@ interface DeleteThemeModalProps {
|
||||
onClose: () => void;
|
||||
themeId: string;
|
||||
onThemeDeleted: () => void;
|
||||
themeName: string;
|
||||
}
|
||||
|
||||
export const DeleteThemeModal = ({
|
||||
@ -15,6 +16,7 @@ export const DeleteThemeModal = ({
|
||||
onClose,
|
||||
themeId,
|
||||
onThemeDeleted,
|
||||
themeName,
|
||||
}: DeleteThemeModalProps) => {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
@ -27,7 +29,7 @@ export const DeleteThemeModal = ({
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("delete_theme")}
|
||||
description={t("delete_theme_description")}
|
||||
description={t("delete_theme_description", { theme: themeName })}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="delete-all-themes-modal__container">
|
||||
|
@ -25,13 +25,15 @@ export const SettingsAppearance = () => {
|
||||
{!themes.length ? (
|
||||
<ThemePlaceholder onListUpdated={loadThemes} />
|
||||
) : (
|
||||
themes.map((theme) => (
|
||||
<ThemeCard
|
||||
key={theme.id}
|
||||
theme={theme}
|
||||
onListUpdated={loadThemes}
|
||||
/>
|
||||
))
|
||||
[...themes]
|
||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
||||
.map((theme) => (
|
||||
<ThemeCard
|
||||
key={theme.id}
|
||||
theme={theme}
|
||||
onListUpdated={loadThemes}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,7 +15,8 @@ export interface Theme {
|
||||
optional2?: HexColorType;
|
||||
};
|
||||
description?: string;
|
||||
author: number;
|
||||
author: string | undefined;
|
||||
authorName: string | undefined;
|
||||
isActive: boolean;
|
||||
code: string;
|
||||
createdAt: Date;
|
||||
|
19
yarn.lock
19
yarn.lock
@ -1917,6 +1917,20 @@
|
||||
lodash "^4.17.15"
|
||||
tmp-promise "^3.0.2"
|
||||
|
||||
"@monaco-editor/loader@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
|
||||
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
|
||||
dependencies:
|
||||
state-local "^1.0.6"
|
||||
|
||||
"@monaco-editor/react@^4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
|
||||
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.4.0"
|
||||
|
||||
"@napi-rs/nice-android-arm-eabi@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936"
|
||||
@ -8719,6 +8733,11 @@ stat-mode@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465"
|
||||
integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==
|
||||
|
||||
state-local@^1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
|
||||
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
|
Loading…
Reference in New Issue
Block a user