@basthon/console-react 0.5.8 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/app.d.ts +6 -1
- package/lib/components/app.js +11 -5
- package/lib/components/button.js +6 -3
- package/lib/components/main-container.d.ts +8 -2
- package/lib/components/main-container.js +8 -3
- package/lib/components/styles.module.scss +33 -2
- package/lib/gui/editor.d.ts +1 -0
- package/lib/gui/editor.js +18 -3
- package/lib/gui/gui.d.ts +23 -0
- package/lib/gui/gui.js +44 -6
- package/lib/hooks/state.d.ts +4 -0
- package/lib/hooks/state.js +4 -0
- package/lib/index.d.ts +9 -2
- package/lib/index.js +7 -1
- package/lib/state-manager.d.ts +6 -2
- package/lib/state-manager.js +15 -7
- package/package.json +1 -1
package/lib/components/app.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { FC, PropsWithChildren } from "react";
|
|
1
|
+
import { FC, PropsWithChildren, type ReactNode } from "react";
|
|
2
2
|
import { type BasthonContextProps } from "@basthon/gui-react";
|
|
3
3
|
import { type Theme } from "../state-manager";
|
|
4
|
+
import { type PanelTitles } from "./main-container";
|
|
4
5
|
interface InnerAppProps {
|
|
5
6
|
initFromStorage?: boolean;
|
|
7
|
+
storedStates?: string[];
|
|
6
8
|
forceTheme?: Theme;
|
|
7
9
|
compactHeader?: boolean;
|
|
10
|
+
header?: ReactNode;
|
|
11
|
+
panelTitles?: PanelTitles;
|
|
8
12
|
onChangeEditor?: () => void;
|
|
9
13
|
}
|
|
10
14
|
interface AppProps extends BasthonContextProps, InnerAppProps {
|
|
@@ -13,4 +17,5 @@ interface AppProps extends BasthonContextProps, InnerAppProps {
|
|
|
13
17
|
* Main app setting up state manager, header, main container and footer.
|
|
14
18
|
*/
|
|
15
19
|
declare const ConsoleApp: FC<PropsWithChildren<AppProps>>;
|
|
20
|
+
export { type AppProps };
|
|
16
21
|
export default ConsoleApp;
|
package/lib/components/app.js
CHANGED
|
@@ -5,36 +5,42 @@ import GUI from "../gui/gui";
|
|
|
5
5
|
import { useGUI } from "../hooks/gui";
|
|
6
6
|
import stateManager from "../state-manager";
|
|
7
7
|
import { DispatchContext } from "../hooks/dispatch";
|
|
8
|
+
import { StateContext } from "../hooks/state";
|
|
8
9
|
import { Header } from "./header";
|
|
9
10
|
import { MainContainer } from "./main-container";
|
|
10
11
|
import { Footer } from "./footer";
|
|
11
12
|
import * as styles from "./styles.module.scss";
|
|
12
|
-
const InnerApp = ({ initFromStorage, forceTheme, compactHeader, onChangeEditor, children, }) => {
|
|
13
|
+
const InnerApp = ({ initFromStorage, storedStates, forceTheme, compactHeader, header, panelTitles, onChangeEditor, children, }) => {
|
|
13
14
|
const gui = useGUI();
|
|
14
15
|
// state
|
|
15
|
-
const [state, dispatch] = stateManager.useReducer(gui.language, initFromStorage ?? true);
|
|
16
|
+
const [state, dispatch] = stateManager.useReducer(gui.language, initFromStorage ?? true, storedStates);
|
|
16
17
|
const language = stateManager.language(state);
|
|
17
18
|
const theme = forceTheme != null ? forceTheme : stateManager.theme(state);
|
|
18
19
|
// set data-theme for basthon-loader footer
|
|
19
20
|
// FIXME: remove this when basthon-loader is a plain react component
|
|
20
21
|
useEffect(() => {
|
|
22
|
+
gui.setTheme(theme);
|
|
21
23
|
document.body.setAttribute("data-theme", theme);
|
|
22
24
|
return () => {
|
|
23
25
|
document.body.removeAttribute("data-theme");
|
|
24
26
|
};
|
|
25
27
|
}, [theme]);
|
|
26
28
|
// build component
|
|
27
|
-
return (
|
|
29
|
+
return (_jsx(DispatchContext.Provider, { value: dispatch, children: _jsxs(StateContext.Provider, { value: { ...state, theme }, children: [children, _jsxs("div", { className: styles.root, children: [header !== undefined ? (header) : (_jsx(Header, { language: language, theme: theme, compact: compactHeader })), stateManager.ready(state) && (_jsx(MainContainer, { language: language, theme: theme, viewMode: stateManager.viewMode(state), rightPanel: stateManager.rightPanel(state), graphicsDirty: stateManager.graphicsDirty(state), editorContent: stateManager.editorContent(state), onChangeEditor: onChangeEditor, compactHeader: compactHeader, panelTitles: panelTitles })), _jsx(Footer, { theme: theme })] })] }) }));
|
|
28
30
|
};
|
|
29
31
|
/**
|
|
30
32
|
* Main app setting up state manager, header, main container and footer.
|
|
31
33
|
*/
|
|
32
|
-
const ConsoleApp = ({ initFromStorage, forceTheme, compactHeader, onChangeEditor, children, ...props }) => {
|
|
33
|
-
|
|
34
|
+
const ConsoleApp = ({ initFromStorage, storedStates, forceTheme, compactHeader, header, panelTitles, onChangeEditor, children, ...props }) => {
|
|
35
|
+
// custom GUI classes should extend the console's GUI
|
|
36
|
+
props.GUI ?? (props.GUI = GUI);
|
|
34
37
|
const innerProps = {
|
|
35
38
|
initFromStorage,
|
|
39
|
+
storedStates,
|
|
36
40
|
forceTheme,
|
|
37
41
|
compactHeader,
|
|
42
|
+
header,
|
|
43
|
+
panelTitles,
|
|
38
44
|
onChangeEditor,
|
|
39
45
|
};
|
|
40
46
|
return (_jsx(BasthonContext, { ...props, children: _jsx(InnerApp, { ...innerProps, children: children }) }));
|
package/lib/components/button.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as styles from "./styles.module.scss";
|
|
3
|
-
const Button = ({ children, titled, blink, ...props }) => {
|
|
4
|
-
|
|
3
|
+
const Button = ({ children, titled, blink, className: extraClassName, ...props }) => {
|
|
4
|
+
const classNames = [styles.btn];
|
|
5
5
|
if (blink === true)
|
|
6
|
-
|
|
6
|
+
classNames.push(styles["btn-eye-catcher"]);
|
|
7
|
+
if (extraClassName != null)
|
|
8
|
+
classNames.push(extraClassName);
|
|
9
|
+
const className = classNames.join(" ");
|
|
7
10
|
return (_jsx("button", { className: className, "data-titled": titled, ...props, children: children }));
|
|
8
11
|
};
|
|
9
12
|
export { Button };
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
1
|
+
import { FC, type ReactNode } from "react";
|
|
2
2
|
import { type Language, type Theme, type ViewMode, type RightPanel } from "../state-manager";
|
|
3
|
+
interface PanelTitles {
|
|
4
|
+
editor?: ReactNode;
|
|
5
|
+
terminal?: ReactNode;
|
|
6
|
+
graphics?: ReactNode;
|
|
7
|
+
}
|
|
3
8
|
interface MainContainerProps {
|
|
4
9
|
language: Language;
|
|
5
10
|
theme: Theme;
|
|
@@ -9,6 +14,7 @@ interface MainContainerProps {
|
|
|
9
14
|
editorContent: string;
|
|
10
15
|
onChangeEditor?: () => void;
|
|
11
16
|
compactHeader?: boolean;
|
|
17
|
+
panelTitles?: PanelTitles;
|
|
12
18
|
}
|
|
13
19
|
declare const MainContainer: FC<MainContainerProps>;
|
|
14
|
-
export { MainContainer };
|
|
20
|
+
export { MainContainer, type PanelTitles };
|
|
@@ -11,13 +11,18 @@ import { faCogs, faFolderOpen, faDownload, faShareAltSquare, faEyeSlash, faUndoA
|
|
|
11
11
|
const MainContainer = (props) => {
|
|
12
12
|
const gui = useGUI();
|
|
13
13
|
const dispatch = useDispatch();
|
|
14
|
-
|
|
14
|
+
const titles = props.panelTitles;
|
|
15
|
+
const rightTitle = props.rightPanel === "graphics" ? titles?.graphics : titles?.terminal;
|
|
16
|
+
return (_jsxs("div", { className: styles.container, "data-theme": props.theme, "data-compact-header": props.compactHeader, "data-titled": titles != null, children: [_jsxs("div", { className: styles["left-panel"], "data-view-mode": props.viewMode, children: [titles?.editor != null && (_jsx("div", { className: styles["panel-title"], children: titles.editor })), _jsx(Editor, { id: "editor", language: props.language, theme: props.theme, content: props.editorContent, onSetContent: (content) => dispatch({ type: "set-editor-content", payload: content }), onChange: () => {
|
|
17
|
+
gui.editorChanged();
|
|
18
|
+
props.onChangeEditor?.();
|
|
19
|
+
} }), _jsxs("div", { className: styles["buttons-container"], children: [_jsxs(Button, { titled: true, id: "run", title: "Ex\u00E9cuter le script", onClick: () => gui.runScript(), children: [_jsx(FontAwesomeIcon, { icon: faCogs, className: styles["btn-logo"] }), "Ex\u00E9cuter"] }), _jsx(Button, { id: "open", title: "Ouvrir un script, charger un module ou un fichier", onClick: () => gui.openFile(), children: _jsx(FontAwesomeIcon, { icon: faFolderOpen, className: styles["btn-logo"] }) }), _jsx(Button, { id: "download", title: "T\u00E9l\u00E9charger le script", onClick: () => gui.download(), children: _jsx(FontAwesomeIcon, { icon: faDownload, className: styles["btn-logo"] }) }), _jsx(Button, { id: "share", title: "Partager ce code", onClick: () => gui.share(), children: _jsx(FontAwesomeIcon, { icon: faShareAltSquare, className: styles["btn-logo"] }) }), props.viewMode === "script-only" && (_jsx(Button, { id: "hide-editor", title: "Afficher seulement la console", onClick: () => dispatch({
|
|
15
20
|
type: "set-view-mode",
|
|
16
21
|
payload: "terminal-only",
|
|
17
|
-
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })), _jsx(Button, { title: "Revenir \u00E0 une version pr\u00E9c\u00E9dente du script", onClick: () => gui.rollback(), children: _jsx(FontAwesomeIcon, { icon: faUndoAlt, className: styles["btn-logo"] }) })] })] }), _jsxs("div", { className: styles["right-panel"], "data-view-mode": props.viewMode, children: [_jsx(Terminal, { "data-right-panel": props.rightPanel }), _jsx(Graphics, { "data-right-panel": props.rightPanel, onContentUpdate: () => dispatch({ type: "graphics-dirty" }) }), _jsxs("div", { className: styles["buttons-container"], children: [_jsx(Button, { title: "Red\u00E9marrer le noyau", onClick: () => gui.restart(), children: _jsx(FontAwesomeIcon, { icon: faBroom, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Afficher la console", onClick: () => dispatch({ type: "set-right-panel", payload: "terminal" }), children: _jsx(FontAwesomeIcon, { icon: faTerminal, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Afficher la vue graphique", onClick: () => dispatch({ type: "set-right-panel", payload: "graphics" }), blink: props.graphicsDirty, children: _jsx(FontAwesomeIcon, { icon: faImage, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Changer le th\u00E8me (sombre/lumineux)", onClick: () => dispatch({ type: "switch-theme" }), children: _jsx(FontAwesomeIcon, { icon: faAdjust, className: styles["btn-logo"] }) }), ["default", "reversed"].includes(props.viewMode) && (_jsxs(_Fragment, { children: [_jsx(Button, { title: "\u00C9changer l'\u00E9diteur et la console", onClick: () => dispatch({ type: "switch-view" }), children: _jsx(FontAwesomeIcon, { icon: faExchangeAlt, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Afficher seulement l'\u00E9diteur", onClick: () => dispatch({
|
|
22
|
+
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })), _jsx(Button, { id: "rollback", title: "Revenir \u00E0 une version pr\u00E9c\u00E9dente du script", onClick: () => gui.rollback(), children: _jsx(FontAwesomeIcon, { icon: faUndoAlt, className: styles["btn-logo"] }) })] })] }), _jsxs("div", { className: styles["right-panel"], "data-view-mode": props.viewMode, children: [rightTitle != null && (_jsx("div", { className: styles["panel-title"], children: rightTitle })), _jsx(Terminal, { id: "shell", "data-right-panel": props.rightPanel }), _jsx(Graphics, { id: "graphics", "data-right-panel": props.rightPanel, onContentUpdate: () => dispatch({ type: "graphics-dirty" }) }), _jsxs("div", { className: styles["buttons-container"], children: [_jsx(Button, { id: "raz", title: "Red\u00E9marrer le noyau", onClick: () => gui.restart(), children: _jsx(FontAwesomeIcon, { icon: faBroom, className: styles["btn-logo"] }) }), _jsx(Button, { id: "btn_shell", title: "Afficher la console", onClick: () => dispatch({ type: "set-right-panel", payload: "terminal" }), children: _jsx(FontAwesomeIcon, { icon: faTerminal, className: styles["btn-logo"] }) }), _jsx(Button, { id: "btn_graph_view", title: "Afficher la vue graphique", onClick: () => dispatch({ type: "set-right-panel", payload: "graphics" }), blink: props.graphicsDirty, children: _jsx(FontAwesomeIcon, { icon: faImage, className: styles["btn-logo"] }) }), _jsx(Button, { id: "darklight", title: "Changer le th\u00E8me (sombre/lumineux)", onClick: () => dispatch({ type: "switch-theme" }), children: _jsx(FontAwesomeIcon, { icon: faAdjust, className: styles["btn-logo"] }) }), ["default", "reversed"].includes(props.viewMode) && (_jsxs(_Fragment, { children: [_jsx(Button, { id: "switch", title: "\u00C9changer l'\u00E9diteur et la console", onClick: () => dispatch({ type: "switch-view" }), children: _jsx(FontAwesomeIcon, { icon: faExchangeAlt, className: styles["btn-logo"] }) }), _jsx(Button, { id: "hide-console", title: "Afficher seulement l'\u00E9diteur", onClick: () => dispatch({
|
|
18
23
|
type: "set-view-mode",
|
|
19
24
|
payload: "script-only",
|
|
20
|
-
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })] })), props.viewMode === "terminal-only" && (_jsx(Button, { title: "Afficher l'\u00E9diteur et la console", onClick: () => dispatch({
|
|
25
|
+
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })] })), props.viewMode === "terminal-only" && (_jsx(Button, { id: "show-editor-console", title: "Afficher l'\u00E9diteur et la console", onClick: () => dispatch({
|
|
21
26
|
type: "set-view-mode",
|
|
22
27
|
payload: "default",
|
|
23
28
|
}), children: _jsx(FontAwesomeIcon, { icon: faColumns, className: styles["btn-logo"] }) }))] })] })] }));
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
--logo-height: 12vh;
|
|
12
12
|
--term-height: 60vh;
|
|
13
13
|
--term-font-size: 12pt;
|
|
14
|
-
--term-font:
|
|
15
|
-
monospace;
|
|
14
|
+
--term-font:
|
|
15
|
+
"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace;
|
|
16
16
|
--term-script-color: var(--python-grey);
|
|
17
17
|
--main-container-width: 90%;
|
|
18
18
|
--main-container-height: 75vh;
|
|
@@ -114,6 +114,7 @@ textarea:focus {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
.left-panel {
|
|
117
|
+
position: relative;
|
|
117
118
|
width: var(--left-container-width);
|
|
118
119
|
margin: auto;
|
|
119
120
|
float: left;
|
|
@@ -151,6 +152,36 @@ textarea:focus {
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
.panel-title {
|
|
156
|
+
z-index: 100;
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: -1.3vw;
|
|
159
|
+
left: 1.5vw;
|
|
160
|
+
margin: 0;
|
|
161
|
+
padding: 0vw 0.7vw 0.2vw 0.7vw;
|
|
162
|
+
color: #fff;
|
|
163
|
+
background-color: var(--python-blue);
|
|
164
|
+
border-radius: var(--corner-radius);
|
|
165
|
+
border: none;
|
|
166
|
+
font-size: var(--main-font-size);
|
|
167
|
+
user-select: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* leave room for panel titles */
|
|
171
|
+
.container[data-titled="true"] {
|
|
172
|
+
margin-top: 1.3vw;
|
|
173
|
+
|
|
174
|
+
.editor {
|
|
175
|
+
box-sizing: border-box;
|
|
176
|
+
border-top: 1vw solid var(--term-bg-color);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.terminal {
|
|
180
|
+
box-sizing: border-box;
|
|
181
|
+
padding-top: 0.7vw;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
154
185
|
%darklighted {
|
|
155
186
|
background-color: var(--term-bg-color);
|
|
156
187
|
--background: var(--term-bg-color);
|
package/lib/gui/editor.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export default class Editor {
|
|
|
7
7
|
private _kernel;
|
|
8
8
|
private _editor;
|
|
9
9
|
private _onSetContent?;
|
|
10
|
+
private _silent;
|
|
10
11
|
constructor(element: HTMLElement, kernel: KernelBase, onSetContent?: (_: string) => void, onChange?: () => void);
|
|
11
12
|
/**
|
|
12
13
|
* Set Ace mode from language.
|
package/lib/gui/editor.js
CHANGED
|
@@ -4,6 +4,8 @@ import ace from "./ace";
|
|
|
4
4
|
*/
|
|
5
5
|
export default class Editor {
|
|
6
6
|
constructor(element, kernel, onSetContent, onChange) {
|
|
7
|
+
// true while content is set programmatically (no onChange call)
|
|
8
|
+
this._silent = false;
|
|
7
9
|
this._kernel = kernel;
|
|
8
10
|
this._onSetContent = onSetContent;
|
|
9
11
|
this._editor = ace.edit(element);
|
|
@@ -14,9 +16,12 @@ export default class Editor {
|
|
|
14
16
|
highlightSelectedWord: true,
|
|
15
17
|
fontSize: "12pt",
|
|
16
18
|
});
|
|
17
|
-
// connect onchange
|
|
19
|
+
// connect onchange (only for user changes)
|
|
18
20
|
if (onChange != null)
|
|
19
|
-
this._editor.session.on("change", () =>
|
|
21
|
+
this._editor.session.on("change", () => {
|
|
22
|
+
if (!this._silent)
|
|
23
|
+
onChange();
|
|
24
|
+
});
|
|
20
25
|
/* editor's completion */
|
|
21
26
|
// removing all completers and keeping track of local completer
|
|
22
27
|
const localCompleter = this._editor.completers[1]; // any hint to detect this dynamically?
|
|
@@ -85,14 +90,24 @@ export default class Editor {
|
|
|
85
90
|
* Dispatch action to set content.
|
|
86
91
|
*/
|
|
87
92
|
setContent(content) {
|
|
93
|
+
// set it directly since state may not change
|
|
94
|
+
// (user modifications are not dispatched)
|
|
95
|
+
this.setContentNoDispatch(content);
|
|
88
96
|
this._onSetContent?.(content);
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
91
99
|
* Set Ace editor content.
|
|
92
100
|
*/
|
|
93
101
|
setContentNoDispatch(content) {
|
|
94
|
-
if (content
|
|
102
|
+
if (content == null)
|
|
103
|
+
return;
|
|
104
|
+
this._silent = true;
|
|
105
|
+
try {
|
|
95
106
|
this._editor.setValue(content, -1);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
this._silent = false;
|
|
110
|
+
}
|
|
96
111
|
}
|
|
97
112
|
/**
|
|
98
113
|
* Get editor's content
|
package/lib/gui/gui.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export default class GUI extends GUIBase {
|
|
|
8
8
|
private _editor?;
|
|
9
9
|
private _term?;
|
|
10
10
|
private _graphics?;
|
|
11
|
+
private _pendingContent?;
|
|
12
|
+
private _theme?;
|
|
11
13
|
constructor(options: GUIOptions);
|
|
12
14
|
/**
|
|
13
15
|
* Set editor.
|
|
@@ -41,6 +43,14 @@ export default class GUI extends GUIBase {
|
|
|
41
43
|
* Set editor's content.
|
|
42
44
|
*/
|
|
43
45
|
setContent(content: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Set the current theme (called by the app on theme change).
|
|
48
|
+
*/
|
|
49
|
+
setTheme(theme: "dark" | "light"): void;
|
|
50
|
+
/**
|
|
51
|
+
* Get the current theme.
|
|
52
|
+
*/
|
|
53
|
+
theme(): Promise<"dark" | "light" | undefined>;
|
|
44
54
|
/**
|
|
45
55
|
* Notify the user with an error.
|
|
46
56
|
*/
|
|
@@ -65,6 +75,19 @@ export default class GUI extends GUIBase {
|
|
|
65
75
|
*/
|
|
66
76
|
rollback(): Promise<void>;
|
|
67
77
|
protected setupUI(options: any): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Called when the editor's content is modified by the user.
|
|
80
|
+
*/
|
|
81
|
+
editorChanged(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Called on ctrl+s (download the script by default).
|
|
84
|
+
*/
|
|
85
|
+
protected onSaveShortcut(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Called before closing the page (backup by default).
|
|
88
|
+
* Return a string to ask the user for confirmation.
|
|
89
|
+
*/
|
|
90
|
+
protected onBeforeUnload(): string | undefined;
|
|
68
91
|
/**
|
|
69
92
|
* Run the editor script in the terminal.
|
|
70
93
|
*/
|
package/lib/gui/gui.js
CHANGED
|
@@ -22,6 +22,10 @@ export default class GUI extends GUIBase {
|
|
|
22
22
|
*/
|
|
23
23
|
setEditor(editor) {
|
|
24
24
|
this._editor = editor;
|
|
25
|
+
if (this._pendingContent != null) {
|
|
26
|
+
editor.setContent(this._pendingContent);
|
|
27
|
+
this._pendingContent = undefined;
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Set terminal.
|
|
@@ -58,13 +62,31 @@ export default class GUI extends GUIBase {
|
|
|
58
62
|
* Get editor's content.
|
|
59
63
|
*/
|
|
60
64
|
content() {
|
|
61
|
-
|
|
65
|
+
if (this._editor == null)
|
|
66
|
+
return this._pendingContent;
|
|
67
|
+
return this._editor.getContent();
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Set editor's content.
|
|
65
71
|
*/
|
|
66
72
|
setContent(content) {
|
|
67
|
-
|
|
73
|
+
// editor not yet available, delay
|
|
74
|
+
if (this._editor == null)
|
|
75
|
+
this._pendingContent = content;
|
|
76
|
+
else
|
|
77
|
+
this._editor.setContent(content);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Set the current theme (called by the app on theme change).
|
|
81
|
+
*/
|
|
82
|
+
setTheme(theme) {
|
|
83
|
+
this._theme = theme;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the current theme.
|
|
87
|
+
*/
|
|
88
|
+
async theme() {
|
|
89
|
+
return this._theme;
|
|
68
90
|
}
|
|
69
91
|
/**
|
|
70
92
|
* Notify the user with an error.
|
|
@@ -135,13 +157,29 @@ export default class GUI extends GUIBase {
|
|
|
135
157
|
if ((event.ctrlKey || event.metaKey) &&
|
|
136
158
|
String.fromCharCode(event.which).toLowerCase() === "s") {
|
|
137
159
|
event.preventDefault();
|
|
138
|
-
this.
|
|
160
|
+
this.onSaveShortcut();
|
|
139
161
|
}
|
|
140
162
|
});
|
|
141
163
|
/* backup before closing */
|
|
142
|
-
window.onbeforeunload = () =>
|
|
143
|
-
|
|
144
|
-
|
|
164
|
+
window.onbeforeunload = () => this.onBeforeUnload();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Called when the editor's content is modified by the user.
|
|
168
|
+
*/
|
|
169
|
+
editorChanged() { }
|
|
170
|
+
/**
|
|
171
|
+
* Called on ctrl+s (download the script by default).
|
|
172
|
+
*/
|
|
173
|
+
onSaveShortcut() {
|
|
174
|
+
this.download();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Called before closing the page (backup by default).
|
|
178
|
+
* Return a string to ask the user for confirmation.
|
|
179
|
+
*/
|
|
180
|
+
onBeforeUnload() {
|
|
181
|
+
this.backup().catch(this.notifyError.bind(this));
|
|
182
|
+
return undefined;
|
|
145
183
|
}
|
|
146
184
|
/**
|
|
147
185
|
* Run the editor script in the terminal.
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
import ConsoleApp from "./components/app";
|
|
2
|
-
|
|
1
|
+
import ConsoleApp, { type AppProps } from "./components/app";
|
|
2
|
+
import GUI from "./gui/gui";
|
|
3
|
+
import { useGUI } from "./hooks/gui";
|
|
4
|
+
import { useDispatch } from "./hooks/dispatch";
|
|
5
|
+
import { useConsoleState } from "./hooks/state";
|
|
6
|
+
import { Button } from "./components/button";
|
|
7
|
+
import { type PanelTitles } from "./components/main-container";
|
|
8
|
+
import { defaultStoredStates, type State, type Action, type Language, type Theme, type ViewMode, type RightPanel } from "./state-manager";
|
|
9
|
+
export { ConsoleApp, GUI, useGUI, useDispatch, useConsoleState, Button, defaultStoredStates, type AppProps, type PanelTitles, type State, type Action, type Language, type Theme, type ViewMode, type RightPanel, };
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import ConsoleApp from "./components/app";
|
|
2
|
-
|
|
2
|
+
import GUI from "./gui/gui";
|
|
3
|
+
import { useGUI } from "./hooks/gui";
|
|
4
|
+
import { useDispatch } from "./hooks/dispatch";
|
|
5
|
+
import { useConsoleState } from "./hooks/state";
|
|
6
|
+
import { Button } from "./components/button";
|
|
7
|
+
import { defaultStoredStates, } from "./state-manager";
|
|
8
|
+
export { ConsoleApp, GUI, useGUI, useDispatch, useConsoleState, Button, defaultStoredStates, };
|
package/lib/state-manager.d.ts
CHANGED
|
@@ -11,14 +11,17 @@ interface State {
|
|
|
11
11
|
rightPanel: RightPanel;
|
|
12
12
|
graphicsDirty: boolean;
|
|
13
13
|
editorContent: string;
|
|
14
|
+
storedStates: string[];
|
|
14
15
|
}
|
|
15
16
|
interface Action {
|
|
16
17
|
type: "init" | "set-theme" | "switch-theme" | "set-view-mode" | "switch-view" | "set-right-panel" | "graphics-dirty" | "set-editor-content";
|
|
17
18
|
payload?: Theme | ViewMode | RightPanel | State | string;
|
|
18
19
|
}
|
|
20
|
+
declare const defaultStoredStates: string[];
|
|
19
21
|
declare const stateManager: {
|
|
20
|
-
useReducer: (language: Language, initFromStorage: boolean) => readonly [{
|
|
22
|
+
useReducer: (language: Language, initFromStorage: boolean, storedStates?: string[]) => readonly [{
|
|
21
23
|
language: Language;
|
|
24
|
+
storedStates: string[];
|
|
22
25
|
ready: boolean;
|
|
23
26
|
theme: Theme;
|
|
24
27
|
viewMode: ViewMode;
|
|
@@ -34,5 +37,6 @@ declare const stateManager: {
|
|
|
34
37
|
graphicsDirty: (state: State) => boolean;
|
|
35
38
|
editorContent: (state: State) => string;
|
|
36
39
|
};
|
|
37
|
-
export { type Dispatch, type Action, type Language, type Theme, type ViewMode, type RightPanel, };
|
|
40
|
+
export { type State, type Dispatch, type Action, type Language, type Theme, type ViewMode, type RightPanel, };
|
|
41
|
+
export { defaultStoredStates };
|
|
38
42
|
export default stateManager;
|
package/lib/state-manager.js
CHANGED
|
@@ -2,6 +2,13 @@ import { useEffect } from "react";
|
|
|
2
2
|
/* we use immer to fake mutable state */
|
|
3
3
|
import { useImmerReducer } from "use-immer";
|
|
4
4
|
import { Storage } from "@basthon/checkpoints";
|
|
5
|
+
/* states to store (by default) */
|
|
6
|
+
const defaultStoredStates = [
|
|
7
|
+
"theme",
|
|
8
|
+
"viewMode",
|
|
9
|
+
"rightPanel",
|
|
10
|
+
"editorContent",
|
|
11
|
+
];
|
|
5
12
|
/* this initial state */
|
|
6
13
|
const initialState = {
|
|
7
14
|
ready: false,
|
|
@@ -11,18 +18,17 @@ const initialState = {
|
|
|
11
18
|
rightPanel: "terminal",
|
|
12
19
|
graphicsDirty: false,
|
|
13
20
|
editorContent: "",
|
|
21
|
+
storedStates: defaultStoredStates,
|
|
14
22
|
};
|
|
15
23
|
/* copy throught JSON, should be sufficient for what we do */
|
|
16
24
|
function copy(obj) {
|
|
17
25
|
return JSON.parse(JSON.stringify(obj));
|
|
18
26
|
}
|
|
19
|
-
/* states to store */
|
|
20
|
-
const storedStates = ["theme", "viewMode", "rightPanel", "editorContent"];
|
|
21
27
|
/* get state from storage */
|
|
22
|
-
const stateFromStorage = async (language) => {
|
|
28
|
+
const stateFromStorage = async (language, storedStates) => {
|
|
23
29
|
const storage = new Storage(`console.${language}.states`);
|
|
24
30
|
await storage.ready();
|
|
25
|
-
const state = { ...initialState, language };
|
|
31
|
+
const state = { ...initialState, language, storedStates };
|
|
26
32
|
for (let key of storedStates) {
|
|
27
33
|
const storageValue = await storage.get(key);
|
|
28
34
|
if (storageValue != null)
|
|
@@ -34,7 +40,7 @@ const stateFromStorage = async (language) => {
|
|
|
34
40
|
const stateToStorage = async (state) => {
|
|
35
41
|
const storage = new Storage(`console.${state.language}.states`);
|
|
36
42
|
await storage.ready();
|
|
37
|
-
for (let key of storedStates)
|
|
43
|
+
for (let key of state.storedStates)
|
|
38
44
|
await storage.set(key, state[key]);
|
|
39
45
|
};
|
|
40
46
|
/* where our global logic takes place */
|
|
@@ -78,17 +84,18 @@ const reducer = (state, action) => {
|
|
|
78
84
|
};
|
|
79
85
|
/* stateManager exposed as API */
|
|
80
86
|
const stateManager = {
|
|
81
|
-
useReducer: (language, initFromStorage) => {
|
|
87
|
+
useReducer: (language, initFromStorage, storedStates = defaultStoredStates) => {
|
|
82
88
|
const [state, dispatch] = useImmerReducer(reducer, {
|
|
83
89
|
...initialState,
|
|
84
90
|
language,
|
|
91
|
+
storedStates,
|
|
85
92
|
});
|
|
86
93
|
// dispatch init from storage
|
|
87
94
|
useEffect(() => {
|
|
88
95
|
(async () => {
|
|
89
96
|
let payload = undefined;
|
|
90
97
|
if (initFromStorage)
|
|
91
|
-
payload = await stateFromStorage(language);
|
|
98
|
+
payload = await stateFromStorage(language, storedStates);
|
|
92
99
|
dispatch({ type: "init", payload });
|
|
93
100
|
})();
|
|
94
101
|
}, []);
|
|
@@ -102,4 +109,5 @@ const stateManager = {
|
|
|
102
109
|
graphicsDirty: (state) => state.graphicsDirty,
|
|
103
110
|
editorContent: (state) => state.editorContent,
|
|
104
111
|
};
|
|
112
|
+
export { defaultStoredStates };
|
|
105
113
|
export default stateManager;
|