mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-15 02:54:07 +00:00
This was an undocumented feature that gets in the way of other shortcuts, and doesn't extend well to the sideview being used for other things in future commits.
176 lines
6.7 KiB
TypeScript
176 lines
6.7 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2020-2026 The Pybricks Authors
|
|
|
|
import 'react-splitter-layout/lib/index.css';
|
|
import './app.scss';
|
|
import { Classes, Spinner } from '@blueprintjs/core';
|
|
import React, { useEffect, useState } from 'react';
|
|
import SplitterLayout from 'react-splitter-layout';
|
|
import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts';
|
|
import Activities from '../activities/Activities';
|
|
import DfuWindowsDriverInstallDialog from '../firmware/dfuWindowsDriverInstallDialog/DfuWindowsDriverInstallDialog';
|
|
import { InstallPybricksDialog } from '../firmware/installPybricksDialog/InstallPybricksDialog';
|
|
import RestoreOfficialDialog from '../firmware/restoreOfficialDialog/RestoreOfficialDialog';
|
|
import { useSettingIsShowDocsEnabled } from '../settings/hooks';
|
|
import SponsorDialog from '../sponsor/SponsorDialog';
|
|
import StatusBar from '../status-bar/StatusBar';
|
|
import Toolbar from '../toolbar/Toolbar';
|
|
import Tour from '../tour/Tour';
|
|
import { docsDefaultPage } from './constants';
|
|
import { useI18n } from './i18n';
|
|
|
|
const Editor = React.lazy(async () => {
|
|
const [sagaModule, componentModule] = await Promise.all([
|
|
import('../editor/sagas'),
|
|
import('../editor/Editor'),
|
|
]);
|
|
|
|
window.dispatchEvent(
|
|
new CustomEvent('pb-lazy-saga', { detail: { saga: sagaModule.default } }),
|
|
);
|
|
|
|
return componentModule;
|
|
});
|
|
|
|
const Terminal = React.lazy(async () => {
|
|
const [sagaModule, componentModule] = await Promise.all([
|
|
import('../terminal/sagas'),
|
|
import('../terminal/Terminal'),
|
|
]);
|
|
|
|
window.dispatchEvent(
|
|
new CustomEvent('pb-lazy-saga', { detail: { saga: sagaModule.default } }),
|
|
);
|
|
|
|
return componentModule;
|
|
});
|
|
|
|
const Docs: React.FunctionComponent = () => {
|
|
return (
|
|
<iframe
|
|
onLoad={(e) => {
|
|
const target = e.target as HTMLIFrameElement;
|
|
const contentWindow = target.contentWindow;
|
|
if (!contentWindow) {
|
|
console.error('could not get iframe content window');
|
|
return;
|
|
}
|
|
|
|
if (document.body.classList.contains(Classes.DARK)) {
|
|
contentWindow.document.documentElement.classList.add(Classes.DARK);
|
|
}
|
|
}}
|
|
src={docsDefaultPage}
|
|
allowFullScreen={true}
|
|
width="100%"
|
|
height="100%"
|
|
/>
|
|
);
|
|
};
|
|
|
|
const App: React.FunctionComponent = () => {
|
|
const i18n = useI18n();
|
|
const { isDarkMode } = useTernaryDarkMode();
|
|
const { isSettingShowDocsEnabled } = useSettingIsShowDocsEnabled();
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const [docsSplit, setDocsSplit] = useLocalStorage('app-docs-split', 30);
|
|
const [terminalSplit, setTerminalSplit] = useLocalStorage('app-terminal-split', 30);
|
|
|
|
// Classes.DARK has to be applied to body element, otherwise it won't
|
|
// affect portals
|
|
useEffect(() => {
|
|
if (!isDarkMode) {
|
|
// no class for light mode, so nothing to do
|
|
return;
|
|
}
|
|
|
|
document.body.classList.add(Classes.DARK);
|
|
return () => document.body.classList.remove(Classes.DARK);
|
|
}, [isDarkMode]);
|
|
|
|
useEffect(() => {
|
|
const listener = (e: KeyboardEvent) => {
|
|
// prevent default browser keyboard shortcuts that we use
|
|
// NB: some of these like 'n' and 'w' cannot be prevented when
|
|
// running "in the browser"
|
|
if (e.ctrlKey && ['d', 'n', 's', 'w'].includes(e.key)) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
addEventListener('keydown', listener);
|
|
return () => removeEventListener('keydown', listener);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="pb-app" onContextMenu={(e) => e.preventDefault()}>
|
|
<div className="pb-app-body">
|
|
<aside
|
|
className="pb-app-activities"
|
|
aria-label={i18n.translate('landmark.activities')}
|
|
>
|
|
<Activities />
|
|
</aside>
|
|
{/* need a container with position: relative; for SplitterLayout since it uses position: absolute; */}
|
|
<div className="pb-app-main" style={{ position: 'relative' }}>
|
|
<SplitterLayout
|
|
customClassName={
|
|
isSettingShowDocsEnabled ? 'pb-show-docs' : 'pb-hide-docs'
|
|
}
|
|
onDragStart={(): void => setIsDragging(true)}
|
|
onDragEnd={(): void => setIsDragging(false)}
|
|
percentage={true}
|
|
secondaryInitialSize={docsSplit}
|
|
onSecondaryPaneSizeChange={setDocsSplit}
|
|
>
|
|
<SplitterLayout
|
|
vertical={true}
|
|
percentage={true}
|
|
secondaryInitialSize={terminalSplit}
|
|
onSecondaryPaneSizeChange={setTerminalSplit}
|
|
>
|
|
<main
|
|
className="pb-app-editor"
|
|
aria-label={i18n.translate('landmark.editor')}
|
|
>
|
|
<Toolbar />
|
|
<React.Suspense
|
|
fallback={<Spinner className="pb-editor" />}
|
|
>
|
|
<Editor />
|
|
</React.Suspense>
|
|
</main>
|
|
<aside
|
|
className="pb-app-terminal"
|
|
aria-label={i18n.translate('landmark.terminal')}
|
|
>
|
|
<React.Suspense
|
|
fallback={<Spinner className="h-100" />}
|
|
>
|
|
<Terminal />
|
|
</React.Suspense>
|
|
</aside>
|
|
</SplitterLayout>
|
|
<aside
|
|
className="pb-app-docs"
|
|
aria-label={i18n.translate('landmark.documentation')}
|
|
>
|
|
{isDragging && <div className="pb-app-docs-drag-helper" />}
|
|
<Docs />
|
|
</aside>
|
|
</SplitterLayout>
|
|
</div>
|
|
</div>
|
|
<StatusBar />
|
|
<Tour />
|
|
<DfuWindowsDriverInstallDialog />
|
|
<InstallPybricksDialog />
|
|
<RestoreOfficialDialog />
|
|
<SponsorDialog />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|