mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: removing crypto from level
This commit is contained in:
parent
47e6d88dd9
commit
0f0e27e2e5
12 changed files with 88 additions and 134 deletions
|
@ -3,7 +3,6 @@ import jwt from "jsonwebtoken";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import type { Auth } from "@types";
|
import type { Auth } from "@types";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
|
|
||||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
@ -11,9 +10,7 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!auth) return null;
|
if (!auth) return null;
|
||||||
const payload = jwt.decode(
|
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||||
Crypto.decrypt(auth.accessToken)
|
|
||||||
) as jwt.JwtPayload;
|
|
||||||
|
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { Crypto, HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import type { Auth } from "@types";
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentToken = await HydraApi.post("/auth/payment", {
|
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||||
refreshToken: Crypto.decrypt(auth.refreshToken),
|
refreshToken: auth.refreshToken,
|
||||||
}).then((response) => response.accessToken);
|
}).then((response) => response.accessToken);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|
|
@ -1,21 +1,10 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
import { Crypto } from "@main/services";
|
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
const getUserPreferences = async () =>
|
const getUserPreferences = async () =>
|
||||||
db
|
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||||
.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
valueEncoding: "json",
|
||||||
valueEncoding: "json",
|
});
|
||||||
})
|
|
||||||
.then((userPreferences) => {
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
|
||||||
userPreferences.realDebridApiToken = Crypto.decrypt(
|
|
||||||
userPreferences.realDebridApiToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return userPreferences;
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEvent("getUserPreferences", getUserPreferences);
|
registerEvent("getUserPreferences", getUserPreferences);
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||||
Crypto,
|
|
||||||
DownloadManager,
|
|
||||||
logger,
|
|
||||||
Ludusavi,
|
|
||||||
startMainLoop,
|
|
||||||
} from "./services";
|
|
||||||
import { RealDebridClient } from "./services/download/real-debrid";
|
import { RealDebridClient } from "./services/download/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
|
@ -27,9 +21,7 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
Aria2.spawn();
|
Aria2.spawn();
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
Crypto.decrypt(userPreferences.realDebridApiToken)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
@ -106,9 +98,7 @@ const migrateFromSqlite = async () => {
|
||||||
|
|
||||||
await db.put(levelKeys.userPreferences, {
|
await db.put(levelKeys.userPreferences, {
|
||||||
...rest,
|
...rest,
|
||||||
realDebridApiToken: realDebridApiToken
|
realDebridApiToken,
|
||||||
? Crypto.encrypt(realDebridApiToken)
|
|
||||||
: null,
|
|
||||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
runAtStartup: rest.runAtStartup === 1,
|
runAtStartup: rest.runAtStartup === 1,
|
||||||
startMinimized: rest.startMinimized === 1,
|
startMinimized: rest.startMinimized === 1,
|
||||||
|
@ -171,8 +161,8 @@ const migrateFromSqlite = async () => {
|
||||||
await db.put<string, Auth>(
|
await db.put<string, Auth>(
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
accessToken: Crypto.encrypt(users[0].accessToken),
|
accessToken: users[0].accessToken,
|
||||||
refreshToken: Crypto.encrypt(users[0].refreshToken),
|
refreshToken: users[0].refreshToken,
|
||||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { safeStorage } from "electron";
|
|
||||||
import { logger } from "./logger";
|
|
||||||
|
|
||||||
export class Crypto {
|
|
||||||
public static encrypt(str: string) {
|
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
|
||||||
return safeStorage.encryptString(str).toString("base64");
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"Encrypt method returned raw string because encryption is not available"
|
|
||||||
);
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static decrypt(b64: string) {
|
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
|
||||||
return safeStorage.decryptString(Buffer.from(b64, "base64"));
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"Decrypt method returned raw string because encryption is not available"
|
|
||||||
);
|
|
||||||
|
|
||||||
return b64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,6 @@ import { isFuture, isToday } from "date-fns";
|
||||||
import { db } from "@main/level";
|
import { db } from "@main/level";
|
||||||
import { levelKeys } from "@main/level/sublevels";
|
import { levelKeys } from "@main/level/sublevels";
|
||||||
import type { Auth, User } from "@types";
|
import type { Auth, User } from "@types";
|
||||||
import { Crypto } from "./crypto";
|
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
|
@ -32,7 +31,9 @@ export class HydraApi {
|
||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
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 = true;
|
||||||
|
|
||||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
private static secondsToMilliseconds(seconds: number) {
|
||||||
|
return seconds * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
private static userAuth: HydraApiUserAuth = {
|
private static userAuth: HydraApiUserAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
|
@ -80,8 +81,8 @@ export class HydraApi {
|
||||||
db.put<string, Auth>(
|
db.put<string, Auth>(
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
accessToken: Crypto.encrypt(accessToken),
|
accessToken,
|
||||||
refreshToken: Crypto.encrypt(refreshToken),
|
refreshToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
|
@ -194,12 +195,8 @@ export class HydraApi {
|
||||||
const user = result.at(1) as User | undefined;
|
const user = result.at(1) as User | undefined;
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: userAuth?.accessToken
|
authToken: userAuth?.accessToken ?? "",
|
||||||
? Crypto.decrypt(userAuth.accessToken)
|
refreshToken: userAuth?.refreshToken ?? "",
|
||||||
: "",
|
|
||||||
refreshToken: userAuth?.refreshToken
|
|
||||||
? Crypto.decrypt(userAuth.refreshToken)
|
|
||||||
: "",
|
|
||||||
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
||||||
subscription: user?.subscription
|
subscription: user?.subscription
|
||||||
? { expiresAt: user.subscription?.expiresAt }
|
? { expiresAt: user.subscription?.expiresAt }
|
||||||
|
@ -248,7 +245,7 @@ export class HydraApi {
|
||||||
levelKeys.auth,
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
...auth,
|
...auth,
|
||||||
accessToken: Crypto.encrypt(accessToken),
|
accessToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
|
|
|
@ -234,7 +234,7 @@ export class WindowManager {
|
||||||
|
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
editorWindow.loadURL(
|
editorWindow.loadURL(
|
||||||
`${process.env["ELECTRON_RENDERER_URL"]}#/editor?themeId=${themeId}`
|
`${process.env["ELECTRON_RENDERER_URL"]}#/theme-editor?themeId=${themeId}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||||
|
|
|
@ -30,7 +30,6 @@ import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-m
|
||||||
|
|
||||||
import { injectCustomCss } from "./helpers";
|
import { injectCustomCss } from "./helpers";
|
||||||
import "./app.scss";
|
import "./app.scss";
|
||||||
import { Theme } from "@types";
|
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -214,22 +213,22 @@ export function App() {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||||
|
|
||||||
channel.onmessage = (event: MessageEvent<number>) => {
|
channel.onmessage = async (event: MessageEvent<number>) => {
|
||||||
const newRepacksCount = event.data;
|
const newRepacksCount = event.data;
|
||||||
window.electron.publishNewRepacksNotification(newRepacksCount);
|
window.electron.publishNewRepacksNotification(newRepacksCount);
|
||||||
updateRepacks();
|
updateRepacks();
|
||||||
|
|
||||||
downloadSourcesTable.toArray().then((downloadSources) => {
|
const downloadSources = await downloadSourcesTable.toArray();
|
||||||
downloadSources
|
|
||||||
.filter((source) => !source.fingerprint)
|
downloadSources
|
||||||
.forEach((downloadSource) => {
|
.filter((source) => !source.fingerprint)
|
||||||
window.electron
|
.forEach(async (downloadSource) => {
|
||||||
.putDownloadSource(downloadSource.objectIds)
|
const { fingerprint } = await window.electron.putDownloadSource(
|
||||||
.then(({ fingerprint }) => {
|
downloadSource.objectIds
|
||||||
downloadSourcesTable.update(downloadSource.id, { fingerprint });
|
);
|
||||||
});
|
|
||||||
});
|
downloadSourcesTable.update(downloadSource.id, { fingerprint });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
|
@ -237,9 +236,9 @@ export function App() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAndApplyTheme = async () => {
|
const loadAndApplyTheme = async () => {
|
||||||
const activeTheme: Theme = await window.electron.getActiveCustomTheme();
|
const activeTheme = await window.electron.getActiveCustomTheme();
|
||||||
|
|
||||||
if (activeTheme.code) {
|
if (activeTheme?.code) {
|
||||||
injectCustomCss(activeTheme.code);
|
injectCustomCss(activeTheme.code);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,9 @@ const Profile = React.lazy(() => import("./pages/profile/profile"));
|
||||||
const Achievements = React.lazy(
|
const Achievements = React.lazy(
|
||||||
() => import("./pages/achievements/achievements")
|
() => import("./pages/achievements/achievements")
|
||||||
);
|
);
|
||||||
const Editor = React.lazy(() => import("./pages/editor/editor"));
|
const ThemeEditor = React.lazy(
|
||||||
|
() => import("./pages/theme-editor/theme-editor")
|
||||||
|
);
|
||||||
|
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
|
@ -107,8 +109,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/editor"
|
path="/theme-editor"
|
||||||
element={<SuspenseWrapper Component={Editor} />}
|
element={<SuspenseWrapper Component={ThemeEditor} />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function DeleteGameModal({
|
||||||
onClose,
|
onClose,
|
||||||
visible,
|
visible,
|
||||||
deleteGame,
|
deleteGame,
|
||||||
}: DeleteGameModalProps) {
|
}: Readonly<DeleteGameModalProps>) {
|
||||||
const { t } = useTranslation("downloads");
|
const { t } = useTranslation("downloads");
|
||||||
|
|
||||||
const handleDeleteGame = () => {
|
const handleDeleteGame = () => {
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
@use "../../scss/globals.scss" as globals;
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
.editor {
|
.theme-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
height: 35px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$dark-background-color;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
|
&--darwin {
|
||||||
|
padding-top: calc(globals.$spacing-unit * 6);
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import "./editor.scss";
|
import "./theme-editor.scss";
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { Theme } from "@types";
|
import { Theme } from "@types";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
@ -10,8 +10,9 @@ import {
|
||||||
ProjectRoadmapIcon,
|
ProjectRoadmapIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
const EditorPage = () => {
|
export default function ThemeEditor() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [theme, setTheme] = useState<Theme | null>(null);
|
const [theme, setTheme] = useState<Theme | null>(null);
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
|
@ -37,29 +38,7 @@ const EditorPage = () => {
|
||||||
}
|
}
|
||||||
}, [themeId]);
|
}, [themeId]);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSave = useCallback(async () => {
|
||||||
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) {
|
if (theme) {
|
||||||
const updatedTheme = {
|
const updatedTheme = {
|
||||||
...theme,
|
...theme,
|
||||||
|
@ -74,13 +53,41 @@ const EditorPage = () => {
|
||||||
window.electron.injectCSS(code);
|
window.electron.injectCSS(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [code, theme]);
|
||||||
|
|
||||||
|
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, handleSave, theme]);
|
||||||
|
|
||||||
|
const handleEditorChange = (value: string | undefined) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setCode(value);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor">
|
<div className="theme-editor">
|
||||||
<div className="editor__header">
|
<div
|
||||||
|
className={cn("theme-editor__header", {
|
||||||
|
"theme-editor__header--darwin": window.electron.platform === "darwin",
|
||||||
|
})}
|
||||||
|
>
|
||||||
<h1>{theme?.name}</h1>
|
<h1>{theme?.name}</h1>
|
||||||
{hasUnsavedChanges && <div className="editor__header__status"></div>}
|
{hasUnsavedChanges && (
|
||||||
|
<div className="theme-editor__header__status"></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === "code" && (
|
{activeTab === "code" && (
|
||||||
|
@ -100,15 +107,15 @@ const EditorPage = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "info" && (
|
{activeTab === "info" && (
|
||||||
<div className="editor__info">
|
<div className="theme-editor__info">
|
||||||
entao mano eu ate fiz isso aqui mas tava feio dms ai deu vergonha e
|
entao mano eu ate fiz isso aqui mas tava feio dms ai deu vergonha e
|
||||||
removi kkkk
|
removi kkkk
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="editor__footer">
|
<div className="theme-editor__footer">
|
||||||
<div className="editor__footer-actions">
|
<div className="theme-editor__footer-actions">
|
||||||
<div className="editor__footer-actions__tabs">
|
<div className="theme-editor__footer-actions__tabs">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleTabChange("code")}
|
onClick={() => handleTabChange("code")}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
@ -135,6 +142,4 @@ const EditorPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default EditorPage;
|
|
Loading…
Add table
Add a link
Reference in a new issue