Skip to content

Welcome screen quickstart improvements #2318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2023 The Pybricks Authors
// Copyright (c) 2021-2024 The Pybricks Authors
import docsPackage from '@pybricks/ide-docs/package.json';

// Definitions for compile-time UI settings.
Expand Down Expand Up @@ -87,3 +87,6 @@ export const zipFileExtension = '.zip';

/** The ZIP file MIME type ('application/zip') */
export const zipFileMimeType = 'application/zip';

/** maximum number of recent file displayed */
export const recentFileCount = 3;
4 changes: 2 additions & 2 deletions src/editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2023 The Pybricks Authors
// Copyright (c) 2020-2024 The Pybricks Authors

import './editor.scss';
import {
Expand Down Expand Up @@ -510,7 +510,7 @@ const Editor: React.FunctionComponent = () => {
<ContextMenu
className={classNames('pb-editor-tabpanel', isEmpty && 'pb-empty')}
role="tabpanel"
aria-label={isEmpty ? i18n.translate('welcome') : fileName}
aria-label={isEmpty ? i18n.translate('welcome.label') : fileName}
// NB: we have to create a new context menu each time it is
// shown in order to get some state, like canUndo and canRedo
// that don't have events to monitor changes.
Expand Down
71 changes: 66 additions & 5 deletions src/editor/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022-2023 The Pybricks Authors
// Copyright (c) 2022-2024 The Pybricks Authors

// welcome screen that is shown when no editor is open.

import { Colors } from '@blueprintjs/core';
import React, { useEffect, useRef } from 'react';
import { Button, Colors } from '@blueprintjs/core';
import { DocumentOpen, Plus } from '@blueprintjs/icons';
import React, { useCallback, useEffect, useRef } from 'react';
import { useDispatch } from 'react-redux';
import Two from 'two.js';
import { useTernaryDarkMode } from 'usehooks-ts';
import { Activity, useActivitiesSelectedActivity } from '../activities/hooks';
import { recentFileCount } from '../app/constants';
import { explorerCreateNewFile } from '../explorer/actions';
import { UUID } from '../fileStorage';
import { useSelector } from '../reducers';
import { editorActivateFile } from './actions';
import { useI18n } from './i18n';
import logoSvg from './logo.svg';
import { RecentFileMetadata } from '.';

const defaultRotation = -Math.PI / 9; // radians
const rotationSpeedIncrement = 0.1; // radians per second
Expand Down Expand Up @@ -61,6 +71,8 @@ type WelcomeProps = {
};

const Welcome: React.FunctionComponent<WelcomeProps> = ({ isVisible }) => {
const i18n = useI18n();
const dispatch = useDispatch();
const stateRef = useRef<State>({
rotation: defaultRotation,
rotationSpeed: 0,
Expand Down Expand Up @@ -93,6 +105,7 @@ const Welcome: React.FunctionComponent<WelcomeProps> = ({ isVisible }) => {

const logo = two.load(logoSvg, (g) => {
g.center();

two.add(logo);
two.play();
});
Expand Down Expand Up @@ -156,15 +169,63 @@ const Welcome: React.FunctionComponent<WelcomeProps> = ({ isVisible }) => {
};
}, [isVisible]);

const [, setSelectedActivity] = useActivitiesSelectedActivity();
const handleOpenNewProject = useCallback(() => {
setSelectedActivity(Activity.Explorer);
dispatch(explorerCreateNewFile());
}, [dispatch, setSelectedActivity]);

const handleOpenExplorer = useCallback(
(uuid: UUID) => {
setSelectedActivity(Activity.Explorer);
dispatch(editorActivateFile(uuid));
},
[dispatch, setSelectedActivity],
);

const recentFiles: readonly RecentFileMetadata[] = useSelector(
(s) => s.editor.recentFiles,
);

const getRecentFileShortCuts = () => (
<>
{recentFiles.slice(0, recentFileCount).map((fitem: RecentFileMetadata) => (
<dl key={fitem.uuid}>
<dt>
{i18n.translate('welcome.openProject', {
fileName: fitem.path,
})}
</dt>
<dd>
<Button
icon={<DocumentOpen />}
onClick={() => handleOpenExplorer(fitem.uuid)}
/>
</dd>
</dl>
))}
</>
);

return (
<div
className="pb-editor-welcome"
ref={elementRef}
onContextMenuCapture={(e) => {
e.stopPropagation();
e.preventDefault();
}}
/>
>
<div className="logo" ref={elementRef}></div>
<div className="shortcuts">
{getRecentFileShortCuts()}
<dl>
<dt>{i18n.translate('welcome.newProject')}</dt>
<dd>
<Button icon={<Plus />} onClick={handleOpenNewProject} />
</dd>
</dl>
</div>
</div>
);
};

Expand Down
12 changes: 11 additions & 1 deletion src/editor/actions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022-2023 The Pybricks Authors
// Copyright (c) 2022-2024 The Pybricks Authors

import { createAction } from '../actions';
import { UUID } from '../fileStorage';
import { RecentFileMetadata } from '.';
export {
didFailToInit as editorCompletionDidFailToInit,
didInit as editorCompletionDidInit,
Expand Down Expand Up @@ -132,3 +133,12 @@ export const editorReplaceFile = createAction((uuid: UUID, value: string) => ({
uuid,
value,
}));

/**
* Requests to replace the value a file in the editor.
* @param files The recent files.
*/
export const editorRecentFiles = createAction((files: RecentFileMetadata[]) => ({
type: 'editor.action.recentFiles',
files,
}));
51 changes: 48 additions & 3 deletions src/editor/editor.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2023 The Pybricks Authors
// Copyright (c) 2020-2024 The Pybricks Authors

// Custom styling for the Editor control.

Expand Down Expand Up @@ -63,13 +63,58 @@
flex: 1 1 auto;
}

.pb-editor-tabpanel.pb-empty {
display: flex;
justify-content: space-around;
}

&-welcome {
display: none;

.pb-editor-tabpanel.pb-empty > & {
display: block;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;

width: 100%;
height: 100%;

.logo {
flex: 1;
display: flex;
min-height: 0;
width: 100%;

svg {
width: 100%;
height: 100%;
object-fit: contain;
min-height: 10;
flex: 1;
}
}
.shortcuts {
padding: 20px;
text-align: center;

border-collapse: separate;
border-spacing: 11px 17px;
dl {
display: table-row;
opacity: 0.8;
}
dt,
dd {
display: table-cell;
vertical-align: middle;
}
dd {
text-align: left;
}
dt {
text-align: right;
}
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 The Pybricks Authors

import { UUID } from '../fileStorage';

/**
* LocalStorage recent files data type.
*/
export type RecentFileMetadata = Readonly<{
/** A globally unique identifier that serves a a file handle. */
uuid: UUID;
/** The path of the file in storage. */
path: string;
}>;
21 changes: 20 additions & 1 deletion src/editor/reducers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// Copyright (c) 2022-2024 The Pybricks Authors

import { Reducer, combineReducers } from 'redux';
import { UUID } from '../fileStorage';
Expand All @@ -8,8 +8,10 @@ import {
editorDidCloseFile,
editorDidCreate,
editorDidOpenFile,
editorRecentFiles,
} from './actions';
import codeCompletion from './redux/codeCompletion';
import { RecentFileMetadata } from '.';

/** Indicates that the code editor is ready for use. */
const isReady: Reducer<boolean> = (state = false, action) => {
Expand Down Expand Up @@ -46,9 +48,26 @@ const openFileUuids: Reducer<readonly UUID[]> = (state = [], action) => {
return state;
};

/** A list of recent files in the order they should be displayed to the user. */
const initialStateRecentFiles = JSON.parse(
localStorage.getItem('editor.recentFiles') || '[]',
) as readonly RecentFileMetadata[];
const recentFiles: Reducer<readonly RecentFileMetadata[]> = (
state = initialStateRecentFiles,
action,
) => {
if (editorRecentFiles.matches(action)) {
return action.files;
//return { ...state, recentFiles: action.files };
}

return state;
};

export default combineReducers({
codeCompletion,
isReady,
activeFileUuid,
openFileUuids,
recentFiles,
});
32 changes: 31 additions & 1 deletion src/editor/sagas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022-2023 The Pybricks Authors
// Copyright (c) 2022-2024 The Pybricks Authors

import type { DatabaseChangeType, IDatabaseChange } from 'dexie-observable/api';
import * as monaco from 'monaco-editor';
Expand All @@ -17,6 +17,7 @@ import {
takeEvery,
} from 'typed-redux-saga/macro';
import { alertsShowAlert } from '../alerts/actions';
import { recentFileCount } from '../app/constants';
import { FileStorageDb, UUID } from '../fileStorage';
import {
fileStorageDidFailToLoadTextFile,
Expand Down Expand Up @@ -62,11 +63,13 @@ import {
editorGetValueResponse,
editorGoto,
editorOpenFile,
editorRecentFiles,
editorReplaceFile,
} from './actions';
import { EditorError } from './error';
import { ActiveFileHistoryManager, OpenFileManager } from './lib';
import { pybricksMicroPythonId } from './pybricksMicroPython';
import { RecentFileMetadata } from '.';

function* handleEditorGetValueRequest(
editor: monaco.editor.ICodeEditor,
Expand Down Expand Up @@ -273,6 +276,33 @@ function* handleEditorActivateFile(

editor.focus();

// store the activated uuid in the recent files queue
let recentFiles = (() => {
try {
return JSON.parse(
localStorage.getItem('editor.recentFiles') ?? '',
) as RecentFileMetadata[];
} catch {
return [];
}
})();

// Check if the file already exists
const fileIndex = recentFiles.findIndex((fitem: RecentFileMetadata) => {
return fitem.uuid === action.uuid;
});
if (fileIndex !== -1) {
recentFiles.splice(fileIndex, 1);
}

const db = yield* getContext<FileStorageDb>('fileStorage');
const metadata = yield* call(() => db.metadata.get(action.uuid));
recentFiles.unshift({ uuid: action.uuid, path: metadata?.path ?? '' }); // Add new (or existing) file to the beginning
recentFiles = [...recentFiles.slice(0, recentFileCount)]; // Keep only the first 10 items
localStorage.setItem('editor.recentFiles', JSON.stringify(recentFiles));
yield* put(editorRecentFiles(recentFiles));

// signal activation done
yield* put(editorDidActivateFile(action.uuid));
} catch (err) {
yield* put(editorDidFailToActivateFile(action.uuid, ensureError(err)));
Expand Down
6 changes: 5 additions & 1 deletion src/editor/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"tablist": {
"label": "Editor"
},
"welcome": "Welcome",
"placeholder": "Write your program here...",
"check": "Check syntax",
"toggleDocs": "Toggle documentation",
Expand All @@ -16,5 +15,10 @@
"docs": {
"show": "Show documentation",
"hide": "Hide documentation"
},
"welcome": {
"label": "Welcome",
"openProject": "Open {fileName}",
"newProject": "Open a new project"
}
}