diff --git a/dar2oar_gui/frontend/global.d.ts b/dar2oar_gui/frontend/global.d.ts new file mode 100644 index 0000000..74e3187 --- /dev/null +++ b/dar2oar_gui/frontend/global.d.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2023 Luma +// SPDX-License-Identifier: MIT or Apache-2.0 +declare module 'monaco-vim' { + class VimMode { + dispose(): void; + initVimMode(editor: editor.IStandaloneCodeEditor, statusbarNode?: Element | null): VimMode; + } + export function initVimMode(editor: editor.IStandaloneCodeEditor, statusbarNode?: HTMLElement): VimMode; +} diff --git a/dar2oar_gui/frontend/src/app/client_layout.tsx b/dar2oar_gui/frontend/src/app/client_layout.tsx new file mode 100644 index 0000000..c100bd8 --- /dev/null +++ b/dar2oar_gui/frontend/src/app/client_layout.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2023 Luma +// SPDX-License-Identifier: MIT or Apache-2.0 +'use client'; +import { CssBaseline, ThemeProvider, createTheme } from '@mui/material'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import NextLink from 'next/link'; + +import Menu from '@/components/navigation'; +import SnackBarProvider from '@/components/providers/snackbar'; + +import type { ComponentProps, ReactNode } from 'react'; + +const darkTheme = createTheme({ + palette: { + mode: 'dark', + }, + components: { + // biome-ignore lint/style/useNamingConvention: + MuiLink: { + defaultProps: { + component: (props: ComponentProps) => , + }, + }, + // biome-ignore lint/style/useNamingConvention: + MuiButtonBase: { + defaultProps: { + // biome-ignore lint/style/useNamingConvention: + LinkComponent: (props: ComponentProps) => , + }, + }, + }, +}); + +interface ClientLayoutProps { + children: ReactNode; +} +const queryClient = new QueryClient(); +export default function ClientLayout({ children }: Readonly) { + return ( + + + + + {children} + + + + ); +} diff --git a/dar2oar_gui/frontend/src/app/globals.css b/dar2oar_gui/frontend/src/app/globals.css index 18e5b31..b6d1049 100644 --- a/dar2oar_gui/frontend/src/app/globals.css +++ b/dar2oar_gui/frontend/src/app/globals.css @@ -39,3 +39,8 @@ a { color-scheme: dark; } } + +.monaco-editor, +.monaco-editor-background { + background-color: #2424248c !important; +} diff --git a/dar2oar_gui/frontend/src/app/layout.tsx b/dar2oar_gui/frontend/src/app/layout.tsx index 2247c63..a18e330 100644 --- a/dar2oar_gui/frontend/src/app/layout.tsx +++ b/dar2oar_gui/frontend/src/app/layout.tsx @@ -11,16 +11,7 @@ import '@/app/globals.css'; const inter = Inter({ subsets: ['latin'] }); -const Menu = dynamic(() => import('@/components/navigation'), { - loading: () => , - ssr: false, -}); - -const ThemeProvider = dynamic(() => import('@/components/providers/theme'), { - loading: () => , - ssr: false, -}); -const SnackBarProvider = dynamic(() => import('@/components/providers/snackbar'), { +const ClientLayout = dynamic(() => import('@/app/client_layout'), { loading: () => , ssr: false, }); @@ -37,13 +28,11 @@ export default function RootLayout({ children }: Props) { return ( - - + {children} {/* To prevents the conversion button from being hidden because the menu is fixed. */}
- - + ); diff --git a/dar2oar_gui/frontend/src/components/editor/code_editor.tsx b/dar2oar_gui/frontend/src/components/editor/code_editor.tsx new file mode 100644 index 0000000..cb6a18a --- /dev/null +++ b/dar2oar_gui/frontend/src/components/editor/code_editor.tsx @@ -0,0 +1,63 @@ +// Copyright (c) 2023 Luma +// SPDX-License-Identifier: MIT or Apache-2.0 +// +// issue: https://github.com/suren-atoyan/monaco-react/issues/136#issuecomment-731420078 +'use client'; +import Editor, { type OnMount } from '@monaco-editor/react'; +import { type ComponentProps, memo, useCallback, useRef } from 'react'; + +import type monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import type { VimMode } from 'monaco-vim'; + +const loadVimKeyBindings: OnMount = (editor, monaco) => { + // setup key bindings before monaco-vim setup + + // setup key bindings + editor.addAction({ + // an unique identifier of the contributed action + id: 'some-unique-id', + // a label of the action that will be presented to the user + label: 'Some label!', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], + + // the method that will be executed when the action is triggered. + run: (editor) => { + alert(`we wanna save something => ${editor.getValue()}`); + }, + }); + + // setup monaco-vim + window.require.config({ + paths: { + 'monaco-vim': 'https://unpkg.com/monaco-vim/dist/monaco-vim', + }, + }); + + window.require(['monaco-vim'], (monacoVim: VimMode) => { + const statusNode = document.querySelector('#status-node'); + monacoVim.initVimMode(editor, statusNode); + }); +}; + +export type CodeEditorProps = ComponentProps & { + readonly vimMode?: boolean; +}; +function CodeEditorInternal({ vimMode = false, onMount, ...params }: CodeEditorProps) { + const editorRef = useRef(null); + + const handleDidMount: OnMount = useCallback( + (editor, monaco) => { + editorRef.current = editor; + if (vimMode) { + loadVimKeyBindings(editor, monaco); + } + + onMount?.(editor, monaco); + }, + [onMount, vimMode], + ); + + return ; +} + +export const CodeEditor = memo(CodeEditorInternal); diff --git a/dar2oar_gui/frontend/src/components/editor/css_editor.tsx b/dar2oar_gui/frontend/src/components/editor/css_editor.tsx index 1f6140b..6a048ed 100644 --- a/dar2oar_gui/frontend/src/components/editor/css_editor.tsx +++ b/dar2oar_gui/frontend/src/components/editor/css_editor.tsx @@ -1,8 +1,7 @@ import InputLabel from '@mui/material/InputLabel'; -import AceEditor from 'react-ace'; +import { CodeEditor } from '@/components/editor'; import { useTranslation } from '@/hooks'; -import { selectEditorMode } from '@/utils/selector'; export type CssEditorProps = { editorMode: string; @@ -14,34 +13,32 @@ export type CssEditorProps = { export const CssEditor = ({ editorMode, setPreset, setStyle, style }: CssEditorProps) => { const { t } = useTranslation(); + const handleCodeChange = (newValue: string | undefined) => { + const value = newValue ?? ''; + setStyle(value); + localStorage.setItem('customCSS', value); + setPreset('0'); + }; + return ( <> {t('custom-css-label')} - { - setStyle(value); - localStorage.setItem('customCSS', value); - setPreset('0'); - }} - placeholder="{ body: url('https://localhost' }" - setOptions={{ useWorker: false }} - style={{ - width: '95%', - backgroundColor: '#2424248c', + ); diff --git a/dar2oar_gui/frontend/src/components/editor/index.ts b/dar2oar_gui/frontend/src/components/editor/index.ts index 0ea170d..ff47af7 100644 --- a/dar2oar_gui/frontend/src/components/editor/index.ts +++ b/dar2oar_gui/frontend/src/components/editor/index.ts @@ -1,12 +1,4 @@ // @index('./*', f => `export * from '${f.path}'`) export * from './css_editor'; export * from './js_editor'; - -// NOTE: These extensions must be loaded after importing AceEditor or they will error -import 'ace-builds/src-min-noconflict/ext-language_tools'; // For completion -import 'ace-builds/src-min-noconflict/keybinding-vim'; -import 'ace-builds/src-min-noconflict/mode-css'; -import 'ace-builds/src-min-noconflict/mode-javascript'; -import 'ace-builds/src-min-noconflict/snippets/css'; -import 'ace-builds/src-min-noconflict/snippets/javascript'; -import 'ace-builds/src-min-noconflict/theme-one_dark'; +export * from './code_editor'; diff --git a/dar2oar_gui/frontend/src/components/editor/js_editor.tsx b/dar2oar_gui/frontend/src/components/editor/js_editor.tsx index b4d6989..5ee8a62 100644 --- a/dar2oar_gui/frontend/src/components/editor/js_editor.tsx +++ b/dar2oar_gui/frontend/src/components/editor/js_editor.tsx @@ -1,9 +1,9 @@ +'use client'; import { Checkbox, FormControlLabel, Grid, Tooltip } from '@mui/material'; import InputLabel from '@mui/material/InputLabel'; -import AceEditor from 'react-ace'; +import { CodeEditor } from '@/components/editor'; import { useInjectScript, useStorageState, useTranslation } from '@/hooks'; -import { selectEditorMode } from '@/utils/selector'; export type JsEditorProps = { editorMode: string; @@ -12,7 +12,7 @@ export type JsEditorProps = { export const JsEditor = ({ editorMode, marginTop }: JsEditorProps) => { const { t } = useTranslation(); - const [script, setScript] = useInjectScript(); + const [script, handleCodeChange] = useInjectScript(); const [runScript, setRunScript] = useStorageState('runScript', 'false'); return ( @@ -56,35 +56,23 @@ export const JsEditor = ({ editorMode, marginTop }: JsEditorProps) => { - { - localStorage.setItem('customJS', value); - setScript(value); + { - const p = document.createElement('p'); - p.innerText = 'Hello'; - document.body.appendChild(p); -)()`} - setOptions={{ useWorker: false }} - style={{ - width: '95%', - backgroundColor: '#2424248c', - }} - tabSize={2} - theme='one_dark' - value={script} + theme='vs-dark' + vimMode={editorMode === 'vim'} + width={'95%'} /> + ); }; diff --git a/dar2oar_gui/frontend/src/components/pages/settings.tsx b/dar2oar_gui/frontend/src/components/pages/settings.tsx index 9eaa38c..7eabba7 100644 --- a/dar2oar_gui/frontend/src/components/pages/settings.tsx +++ b/dar2oar_gui/frontend/src/components/pages/settings.tsx @@ -3,8 +3,9 @@ import TabContext from '@mui/lab/TabContext'; import TabList from '@mui/lab/TabList'; import TabPanel from '@mui/lab/TabPanel'; -import { Box, Button, Grid } from '@mui/material'; +import { Box, Button, Grid, Skeleton } from '@mui/material'; import Tab from '@mui/material/Tab'; +import { Suspense, type SyntheticEvent } from 'react'; import { ExportBackupButton, ImportBackupButton, ImportLangButton } from '@/components/buttons'; import { CssEditor, type CssEditorProps, JsEditor } from '@/components/editor'; @@ -22,8 +23,6 @@ import { type EditorMode, selectEditorMode } from '@/utils/selector'; import packageJson from '@/../../package.json'; -import type { SyntheticEvent } from 'react'; - export default function Settings() { useLocale(); const [editorMode, setEditorMode] = useStorageState('editorMode', 'default'); @@ -31,7 +30,7 @@ export default function Settings() { const [style, setStyle] = useDynStyle(); const validEditorMode = selectEditorMode(editorMode); - const setEditorKeyMode = (editorMode: EditorMode) => setEditorMode(editorMode ?? 'default'); + const setEditorKeyMode = (editorMode: EditorMode) => setEditorMode(editorMode); return ( - - + }> + + + }> + + diff --git a/dar2oar_gui/frontend/src/components/providers/theme.tsx b/dar2oar_gui/frontend/src/components/providers/theme.tsx deleted file mode 100644 index a8bd388..0000000 --- a/dar2oar_gui/frontend/src/components/providers/theme.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { CssBaseline } from '@mui/material'; -import { ThemeProvider as ThemeProviderInner, createTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { useMemo } from 'react'; - -import type { ReactNode } from 'react'; - -export default function ThemeProvider({ children }: Readonly<{ children: ReactNode }>) { - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const theme = useMemo( - () => - createTheme({ - palette: { - mode: prefersDarkMode ? 'dark' : 'light', - }, - }), - [prefersDarkMode], - ); - - return ( - - - {children} - - ); -} diff --git a/dar2oar_gui/frontend/src/hooks/dyn_style.ts b/dar2oar_gui/frontend/src/hooks/dyn_style.ts index f223cea..064222a 100644 --- a/dar2oar_gui/frontend/src/hooks/dyn_style.ts +++ b/dar2oar_gui/frontend/src/hooks/dyn_style.ts @@ -1,4 +1,4 @@ -import { useEffect, useInsertionEffect, useState } from 'react'; +import { useCallback, useEffect, useInsertionEffect, useState } from 'react'; import { notify } from '@/components/notifications'; import { localStorageManager } from '@/utils/local_storage_manager'; @@ -44,6 +44,11 @@ export function useInjectScript(initialState = initScript()) { const [script, setScript] = useState(initialState); const [pathname, setPathname] = useState(null); + const handleScriptChange = useCallback((newValue: string | undefined) => { + setScript(newValue ?? ''); + localStorage.setItem('customJS', newValue ?? ''); + }, []); + useEffect(() => { const scriptElement = document.createElement('script'); if (localStorage.getItem('runScript') === 'true') { @@ -68,5 +73,5 @@ export function useInjectScript(initialState = initScript()) { }; }, [script, pathname]); - return [script, setScript] as const; + return [script, handleScriptChange] as const; } diff --git a/package-lock.json b/package-lock.json index e497493..d712005 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,17 @@ "version": "0.7.0", "license": "MIT", "dependencies": { + "@monaco-editor/react": "^4.6.0", "@mui/icons-material": "^5.15.18", "@mui/lab": "5.0.0-alpha.170", "@mui/material": "^5.15.18", + "@tanstack/react-query": "^5.40.0", "@tauri-apps/api": "^1.5.6", - "ace-builds": "^1.34.1", "i18next": "^23.11.5", + "monaco-vim": "^0.4.1", "next": "14.2.3", "notistack": "^3.0.1", "react": "18.3.1", - "react-ace": "^11.0.1", "react-dom": "18.3.1", "react-hook-form": "^7.51.5", "react-i18next": "^14.1.2" @@ -2240,6 +2241,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -3247,6 +3272,30 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tanstack/query-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", + "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.0.tgz", + "integrity": "sha512-iv/W0Axc4aXhFzkrByToE1JQqayxTPNotCoSCnarR/A1vDIHaoKpg7FTIfP3Ev2mbKn1yrxq0ZKYUdLEJxs6Tg==", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@tauri-apps/api": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.6.tgz", @@ -4127,11 +4176,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, - "node_modules/ace-builds": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.34.1.tgz", - "integrity": "sha512-hwRzr6BkRwsq5A19yA9E36KNNtn0+zESYolnWK3TADJsWVQS0T24nvbgdjXwqk2JEMQXE4PlqAw+ZgprvFtKjw==" - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -5306,11 +5350,6 @@ "node": ">=8" } }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -8589,16 +8628,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8794,6 +8823,20 @@ "ufo": "^1.5.3" } }, + "node_modules/monaco-editor": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz", + "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==", + "peer": true + }, + "node_modules/monaco-vim": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.1.tgz", + "integrity": "sha512-+cy0TE9xarjLIgUexqxIEbat3K1l7WbiFSLZKAO2kYl1qFRvkeWn4ro/C4c6dK0i9+WQKUC4Dhu/nyCbZfA37w==", + "peerDependencies": { + "monaco-editor": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9581,22 +9624,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-ace": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-11.0.1.tgz", - "integrity": "sha512-ulk2851Fx2j59AAahZHTe7rmQ5bITW1xytskAt11F8dv3rPLtdwBXCyT2qSbRnJvOq8UpuAhWO4/JhKGqQBEDA==", - "dependencies": { - "ace-builds": "^1.32.8", - "diff-match-patch": "^1.0.5", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -10197,6 +10224,11 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/std-env": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", diff --git a/package.json b/package.json index bc82141..d748266 100644 --- a/package.json +++ b/package.json @@ -19,16 +19,17 @@ "tauri": "tauri" }, "dependencies": { + "@monaco-editor/react": "^4.6.0", "@mui/icons-material": "^5.15.18", "@mui/lab": "5.0.0-alpha.170", "@mui/material": "^5.15.18", + "@tanstack/react-query": "^5.40.0", "@tauri-apps/api": "^1.5.6", - "ace-builds": "^1.34.1", "i18next": "^23.11.5", + "monaco-vim": "^0.4.1", "next": "14.2.3", "notistack": "^3.0.1", "react": "18.3.1", - "react-ace": "^11.0.1", "react-dom": "18.3.1", "react-hook-form": "^7.51.5", "react-i18next": "^14.1.2" @@ -50,5 +51,8 @@ "typescript": "5.4.5", "vite-tsconfig-paths": "^4.3.2", "vitest": "1.6.0" + }, + "overrides": { + "monaco-editor": "^0.49.0" } }