From 335b4427b73b0e7a088aa3588841c9f6d687198c Mon Sep 17 00:00:00 2001 From: Attila Farago Date: Sun, 13 Oct 2024 01:47:49 +0200 Subject: [PATCH 1/4] feat: improve welcome screen ux --- src/editor/Welcome.tsx | 42 +++++++++++++++++++++++++++++++------ src/editor/editor.scss | 47 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/editor/Welcome.tsx b/src/editor/Welcome.tsx index 7b7cbd9a..6b8d28f4 100644 --- a/src/editor/Welcome.tsx +++ b/src/editor/Welcome.tsx @@ -1,12 +1,16 @@ // 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 { Document, 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 { explorerCreateNewFile } from '../explorer/actions'; import logoSvg from './logo.svg'; const defaultRotation = -Math.PI / 9; // radians @@ -61,6 +65,7 @@ type WelcomeProps = { }; const Welcome: React.FunctionComponent = ({ isVisible }) => { + const dispatch = useDispatch(); const stateRef = useRef({ rotation: defaultRotation, rotationSpeed: 0, @@ -104,7 +109,7 @@ const Welcome: React.FunctionComponent = ({ isVisible }) => { }); logo.fill = fillColorRef.current; - logo.scale = Math.min(two.width, two.height) / 80; + logo.scale = Math.min(two.width, two.height) / 90; logo.rotation = stateRef.current.rotation; two.scene.position.x = two.width / 2; @@ -156,15 +161,40 @@ const Welcome: React.FunctionComponent = ({ isVisible }) => { }; }, [isVisible]); + const [, setSelectedActivity] = useActivitiesSelectedActivity(); + const handleOpenNewProject = useCallback(() => { + setSelectedActivity(Activity.Explorer); + dispatch(explorerCreateNewFile()); + }, [dispatch, setSelectedActivity]); + + const handleOpenExplorer = useCallback(() => { + setSelectedActivity(Activity.Explorer); + }, [setSelectedActivity]); + return (
{ e.stopPropagation(); e.preventDefault(); }} - /> + > +
+
+
+
Open existing project
+
+
+
+
+
Open a new project
+
+
+
+
+
); }; diff --git a/src/editor/editor.scss b/src/editor/editor.scss index 9142f662..08b3adf0 100644 --- a/src/editor/editor.scss +++ b/src/editor/editor.scss @@ -63,13 +63,56 @@ 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; + + // display: block; width: 100%; height: 100%; + // max-width: 290px; + + .logo { + width: 100%; + height: 100%; + position: absolute; + top: -2%; + + svg { + overflow: visible !important; + } + } + .shortcuts { + position: relative; + top: +6%; + + 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; + } + } } } From c2cb457832a60f964d5793c600d355546f2f82bc Mon Sep 17 00:00:00 2001 From: Attila Farago Date: Sun, 13 Oct 2024 02:11:49 +0200 Subject: [PATCH 2/4] improve: use i18n --- src/editor/Editor.tsx | 4 ++-- src/editor/Welcome.tsx | 6 ++++-- src/editor/translations/en.json | 6 +++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index 3f6a438a..16711198 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -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 { @@ -510,7 +510,7 @@ const Editor: React.FunctionComponent = () => { = ({ isVisible }) => { + const i18n = useI18n(); const dispatch = useDispatch(); const stateRef = useRef({ rotation: defaultRotation, @@ -182,13 +184,13 @@ const Welcome: React.FunctionComponent = ({ isVisible }) => {
-
Open existing project
+
{i18n.translate('welcome.openProject')}
-
Open a new project
+
{i18n.translate('welcome.newProject')}
diff --git a/src/editor/translations/en.json b/src/editor/translations/en.json index 28d4fa96..b99c6249 100644 --- a/src/editor/translations/en.json +++ b/src/editor/translations/en.json @@ -2,7 +2,6 @@ "tablist": { "label": "Editor" }, - "welcome": "Welcome", "placeholder": "Write your program here...", "check": "Check syntax", "toggleDocs": "Toggle documentation", @@ -16,5 +15,10 @@ "docs": { "show": "Show documentation", "hide": "Hide documentation" + }, + "welcome": { + "label": "Welcome", + "openProject": "Open existing project", + "newProject": "Open a new project" } } From 4a2ba2bab903e1a657abc4802769720e15a482b4 Mon Sep 17 00:00:00 2001 From: Attila Farago Date: Mon, 14 Oct 2024 01:05:45 +0200 Subject: [PATCH 3/4] improve: welcome page list of recent files work in progress, welcome page to refresh dynamically --- src/app/constants.ts | 5 +++- src/editor/Welcome.tsx | 45 +++++++++++++++++++++++++-------- src/editor/editor.scss | 20 ++++++++------- src/editor/index.ts | 14 ++++++++++ src/editor/sagas.ts | 30 +++++++++++++++++++++- src/editor/translations/en.json | 2 +- 6 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 src/editor/index.ts diff --git a/src/app/constants.ts b/src/app/constants.ts index 90d4addc..d5860a90 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -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. @@ -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; diff --git a/src/editor/Welcome.tsx b/src/editor/Welcome.tsx index 9c6779eb..bccba546 100644 --- a/src/editor/Welcome.tsx +++ b/src/editor/Welcome.tsx @@ -8,11 +8,15 @@ import { Document, 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 { useLocalStorage, 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 { 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 @@ -100,6 +104,7 @@ const Welcome: React.FunctionComponent = ({ isVisible }) => { const logo = two.load(logoSvg, (g) => { g.center(); + two.add(logo); two.play(); }); @@ -111,7 +116,7 @@ const Welcome: React.FunctionComponent = ({ isVisible }) => { }); logo.fill = fillColorRef.current; - logo.scale = Math.min(two.width, two.height) / 90; + logo.scale = Math.min(two.width, two.height) / 80; logo.rotation = stateRef.current.rotation; two.scene.position.x = two.width / 2; @@ -169,9 +174,34 @@ const Welcome: React.FunctionComponent = ({ isVisible }) => { dispatch(explorerCreateNewFile()); }, [dispatch, setSelectedActivity]); - const handleOpenExplorer = useCallback(() => { + //useCallback + const handleOpenExplorer = (uuid: UUID) => { setSelectedActivity(Activity.Explorer); - }, [setSelectedActivity]); + dispatch(editorActivateFile(uuid)); + }; + + const [editorRecentFiles] = useLocalStorage('editor.recentFiles', []); + const getRecentFileShortCuts = () => ( + <> + {editorRecentFiles + .slice(0, recentFileCount) + .map((fitem: RecentFileMetadata) => ( +
+
+ {i18n.translate('welcome.openProject', { + fileName: fitem.path, + })} +
+
+
+
+ ))} + + ); return (
= ({ isVisible }) => { >
-
-
{i18n.translate('welcome.openProject')}
-
-
-
+ {getRecentFileShortCuts()}
{i18n.translate('welcome.newProject')}
diff --git a/src/editor/editor.scss b/src/editor/editor.scss index 08b3adf0..3bdeefd9 100644 --- a/src/editor/editor.scss +++ b/src/editor/editor.scss @@ -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. @@ -76,24 +76,26 @@ justify-content: center; align-items: center; - // display: block; width: 100%; height: 100%; - // max-width: 290px; .logo { + flex: 1; + display: flex; + min-height: 0; width: 100%; - height: 100%; - position: absolute; - top: -2%; svg { - overflow: visible !important; + width: 100%; + height: 100%; + object-fit: contain; + min-height: 10; + flex: 1; } } .shortcuts { - position: relative; - top: +6%; + padding: 20px; + text-align: center; border-collapse: separate; border-spacing: 11px 17px; diff --git a/src/editor/index.ts b/src/editor/index.ts new file mode 100644 index 00000000..039ff557 --- /dev/null +++ b/src/editor/index.ts @@ -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; +}>; diff --git a/src/editor/sagas.ts b/src/editor/sagas.ts index b74c3676..746fa9e9 100644 --- a/src/editor/sagas.ts +++ b/src/editor/sagas.ts @@ -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'; @@ -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, @@ -67,6 +68,7 @@ import { import { EditorError } from './error'; import { ActiveFileHistoryManager, OpenFileManager } from './lib'; import { pybricksMicroPythonId } from './pybricksMicroPython'; +import { RecentFileMetadata } from '.'; function* handleEditorGetValueRequest( editor: monaco.editor.ICodeEditor, @@ -273,6 +275,32 @@ 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('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(recentFileCount)]; // Keep only the first 10 items + localStorage.setItem('editor.recentFiles', JSON.stringify(recentFiles)); + + // signal activation done yield* put(editorDidActivateFile(action.uuid)); } catch (err) { yield* put(editorDidFailToActivateFile(action.uuid, ensureError(err))); diff --git a/src/editor/translations/en.json b/src/editor/translations/en.json index b99c6249..42cb5755 100644 --- a/src/editor/translations/en.json +++ b/src/editor/translations/en.json @@ -18,7 +18,7 @@ }, "welcome": { "label": "Welcome", - "openProject": "Open existing project", + "openProject": "Open {fileName}", "newProject": "Open a new project" } } From 4020cbaf441fd89b9cc15408f57fdd754dfd7c5e Mon Sep 17 00:00:00 2001 From: Attila Farago Date: Mon, 14 Oct 2024 16:36:00 +0200 Subject: [PATCH 4/4] improve: handle recentfiles properly with startup --- src/editor/Welcome.tsx | 54 +++++++++++++++++++++++------------------- src/editor/actions.ts | 12 +++++++++- src/editor/reducers.ts | 21 +++++++++++++++- src/editor/sagas.ts | 4 +++- 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/editor/Welcome.tsx b/src/editor/Welcome.tsx index bccba546..96c7a9f2 100644 --- a/src/editor/Welcome.tsx +++ b/src/editor/Welcome.tsx @@ -4,15 +4,16 @@ // welcome screen that is shown when no editor is open. import { Button, Colors } from '@blueprintjs/core'; -import { Document, Plus } from '@blueprintjs/icons'; +import { DocumentOpen, Plus } from '@blueprintjs/icons'; import React, { useCallback, useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; import Two from 'two.js'; -import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts'; +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'; @@ -174,32 +175,35 @@ const Welcome: React.FunctionComponent = ({ isVisible }) => { dispatch(explorerCreateNewFile()); }, [dispatch, setSelectedActivity]); - //useCallback - const handleOpenExplorer = (uuid: UUID) => { - setSelectedActivity(Activity.Explorer); - dispatch(editorActivateFile(uuid)); - }; + const handleOpenExplorer = useCallback( + (uuid: UUID) => { + setSelectedActivity(Activity.Explorer); + dispatch(editorActivateFile(uuid)); + }, + [dispatch, setSelectedActivity], + ); + + const recentFiles: readonly RecentFileMetadata[] = useSelector( + (s) => s.editor.recentFiles, + ); - const [editorRecentFiles] = useLocalStorage('editor.recentFiles', []); const getRecentFileShortCuts = () => ( <> - {editorRecentFiles - .slice(0, recentFileCount) - .map((fitem: RecentFileMetadata) => ( -
-
- {i18n.translate('welcome.openProject', { - fileName: fitem.path, - })} -
-
-
-
- ))} + {recentFiles.slice(0, recentFileCount).map((fitem: RecentFileMetadata) => ( +
+
+ {i18n.translate('welcome.openProject', { + fileName: fitem.path, + })} +
+
+
+
+ ))} ); diff --git a/src/editor/actions.ts b/src/editor/actions.ts index 497eedf2..8ce8c14b 100644 --- a/src/editor/actions.ts +++ b/src/editor/actions.ts @@ -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, @@ -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, +})); diff --git a/src/editor/reducers.ts b/src/editor/reducers.ts index 6ae1acc4..fcf6ac44 100644 --- a/src/editor/reducers.ts +++ b/src/editor/reducers.ts @@ -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'; @@ -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 = (state = false, action) => { @@ -46,9 +48,26 @@ const openFileUuids: Reducer = (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 = ( + 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, }); diff --git a/src/editor/sagas.ts b/src/editor/sagas.ts index 746fa9e9..acdbbdc3 100644 --- a/src/editor/sagas.ts +++ b/src/editor/sagas.ts @@ -63,6 +63,7 @@ import { editorGetValueResponse, editorGoto, editorOpenFile, + editorRecentFiles, editorReplaceFile, } from './actions'; import { EditorError } from './error'; @@ -297,8 +298,9 @@ function* handleEditorActivateFile( const db = yield* getContext('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(recentFileCount)]; // Keep only the first 10 items + 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));