diff --git a/package.json b/package.json index a21f712..e4677dc 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,7 @@ "@types/react-helmet": "^6.1.2", "@types/react-router-dom": "^5.1.6", "@types/react-test-renderer": "^16.9.3", + "@types/uuid": "^8.3.1", "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", @@ -280,7 +281,8 @@ "react-router-dom": "^5.2.0", "regenerator-runtime": "^0.13.5", "source-map-support": "^0.5.19", - "sql-formatter": "^4.0.2" + "sql-formatter": "^4.0.2", + "uuid": "^8.3.2" }, "devEngines": { "node": ">=10.x", diff --git a/src/components/Main.tsx b/src/components/Main.tsx index 40fb942..599e6c1 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -1,9 +1,9 @@ import React, { ComponentType, ReactElement, useEffect, useState } from 'react'; -import { NavLink, Route, useHistory } from 'react-router-dom'; +import { NavLink, Route, useHistory, useLocation } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Helmet } from 'react-helmet'; - import { ipcRenderer } from 'electron'; +import { v4 as uuidv4 } from 'uuid'; import classNames from 'classnames'; import MarkdownToHtml from './markdown/MarkdownToHtml'; import UnixTimestamp from './timestamp/UnixTimestamp'; @@ -16,6 +16,7 @@ import JsonFormatter from './json/JsonFormatter'; import QRCodeReader from './qrcode/QrCodeReader'; import RegexTester from './regex/RegexTester'; import JwtDebugger from './jwt/JwtDebugger'; +import CustomScript from './custom-script/CustomScript'; import Auto from './auto/Auto'; import CronEditor from './cron/Cron'; import JsConsole from './notebook/JavaScript'; @@ -166,7 +167,10 @@ const Main = () => { const [routes, setRoutes] = useState([]); const [search, setSearch] = useState(''); const [editMenu, setEditMenu] = useState(false); + const [activeMenuItemPath, setActiveMenuItemPath] = useState(''); + const [activeMenuItemName, setActiveMenuItemName] = useState(''); const history = useHistory(); + const location = useLocation(); const handleSearch = (e: { target: { value: string } }) => { setSearch(e.target.value); @@ -196,6 +200,10 @@ const Main = () => { } }, [allRoutes]); + useEffect(() => { + setActiveMenuItemPath(''); + }, [location]); + useEffect(() => { const routeMap: Record = defaultRoutes.reduce( (a, b) => ({ ...a, [b.path]: b.show }), @@ -225,6 +233,38 @@ const Main = () => { }); }, []); + const handleAddNewMenuItem = () => { + const id = uuidv4(); + const routeList = [ + ...allRoutes, + { + icon: , + path: `/custom-script-${id}`, + name: `Custom Script ${id.slice(0, 5)}`, + show: true, + Component: CustomScript, + }, + ]; + setAllRoutes(routeList); + }; + + const handleDeleteMenuItem = (path: MenuItem['path']) => { + setAllRoutes(allRoutes.filter((r) => r.path !== activeMenuItemPath)); + setActiveMenuItemPath(''); + setActiveMenuItemName(''); + ipcRenderer.invoke('delete-store', path); + }; + + const handleSaveMenuItemEdit = () => { + setAllRoutes( + allRoutes.map((r) => + r.path === activeMenuItemPath ? { ...r, name: activeMenuItemName } : r + ) + ); + setActiveMenuItemPath(''); + setActiveMenuItemName(''); + }; + return (
@@ -249,7 +289,10 @@ const Main = () => { )} setEditMenu(!editMenu)} + onClick={() => { + setEditMenu(!editMenu); + if (editMenu) handleSaveMenuItemEdit(); + }} className={classNames({ 'text-gray-400 cursor-pointer hover:text-gray-600': true, 'text-blue-500 hover:text-blue-600': editMenu, @@ -268,30 +311,80 @@ const Main = () => { key={path} className="flex items-center justify-between space-x-2" > - - {icon} - {name} - - {editMenu && ( + {(activeMenuItemPath === path && ( - setAllRoutes( - allRoutes.map((r) => - r.path === path ? { ...r, show: !show } : r - ) - ) - } - className="w-4 h-4 rounded cursor-pointer" + className="flex items-center justify-start flex-1 px-3 py-1 mb-1 space-x-1 rounded-lg" + value={activeMenuItemName} + onChange={(evt) => { + setActiveMenuItemName(evt.target.value); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSaveMenuItemEdit(); + } + }} /> + )) || ( + + {icon} + {name} + + )} + {editMenu && ( + <> + {location.pathname === path && path.startsWith('/custom-') && ( + <> + {(!activeMenuItemPath && ( + + )) || ( + + )} + + )} + + setAllRoutes( + allRoutes.map((r) => + r.path === path ? { ...r, show: !show } : r + ) + ) + } + className="w-4 h-4 rounded cursor-pointer" + /> + )} ))} + {editMenu && ( + + )}
diff --git a/src/components/custom-script/CustomScript.tsx b/src/components/custom-script/CustomScript.tsx new file mode 100644 index 0000000..2f5aed7 --- /dev/null +++ b/src/components/custom-script/CustomScript.tsx @@ -0,0 +1,150 @@ +/* eslint-disable no-eval */ +import React, { useState, useEffect, useCallback } from 'react'; +import { ipcRenderer, clipboard } from 'electron'; +import { UnControlled as CodeMirror } from 'react-codemirror2'; +import { useLocation } from 'react-router-dom'; + +require('codemirror/mode/javascript/javascript'); + +const defaultFunc = `function(x) { + return x; +}`; + +const CustomScript = () => { + const [customFunc, setCustomFunc] = useState(defaultFunc); + const [output, setOutput] = useState(); + const [input, setInput] = useState("'your argument'"); + + const [opening, setOpening] = useState(false); + const [running, setRunning] = useState(false); + const location = useLocation(); + + useEffect(() => { + (async () => { + try { + const { + customFunction, + defaultInput, + } = await ipcRenderer.invoke('get-store', { key: location.pathname }); + setCustomFunc(customFunction); + setInput(defaultInput); + } catch (e) { + // do nothing + } + })(); + }, []); + + const handleOpen = async () => { + setOpening(true); + const filters = [{ name: 'JavaScript Files', extensions: ['js'] }]; + const c = await ipcRenderer.invoke('open-file', filters); + setCustomFunc(Buffer.from(c).toString()); + setOpening(false); + }; + + const handleClipboard = () => { + setCustomFunc(clipboard.readText()); + }; + + const getOutPut = () => { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function(`return (${customFunc})(${input})`)(); + }; + + const handleRun = () => { + setRunning(true); + try { + setOutput(getOutPut()); + } catch (e) { + setOutput(e.message); + } + setRunning(false); + }; + + const handleSave = useCallback(async () => { + try { + await ipcRenderer.invoke('set-store', { + key: location.pathname, + value: { + customFunction: customFunc, + defaultInput: input, + }, + }); + } catch (e) { + // eslint-disable-next-line no-alert + alert(e.message); + } + }, [customFunc, input, location.pathname]); + + useEffect(() => { + handleSave(); + }, [handleSave]); + + return ( +
+
+ + + + + + + + + +
+
+ { + setCustomFunc(value); + }} + /> + { + setInput(value); + }} + /> +