Persist state in localStorage for installed PWAs
This commit is contained in:
parent
39edf6d28b
commit
8c53927f53
9 changed files with 90 additions and 21 deletions
|
|
@ -18,6 +18,7 @@ Licenses: see [LICENSES](./LICENSE).
|
|||
- Autocomplete of imports
|
||||
- Autocomplete of symbols / function calls (pseudo-parses file and its transitive imports)
|
||||
- Responsive layout (but editing on iOS is still a pain, will address that soon). On small screens editor and viewer are stacked onto each other, while on larger screens they can be side-by-side
|
||||
- Installable as a PWA (then persists edits in localStorage instead of the hash fragment). On iOS just open the sharing panel and tap "Add to Home Screen".
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ Licenses: see [LICENSES](./LICENSE).
|
|||
- Better names for downloads (matching source)
|
||||
- Model /home fs in shared state. have two clear paths: /libraries for builtins, and /home for user data. State pointing to /libraries paths needs not store the data except if there's overrides (flagged as modifications in the file picker)
|
||||
- Drag and drop of files (SCAD, STL, etc) and Zip archives. For assets, auto insert the corresponding import.
|
||||
- Proper PWA w/ File opening / association to *.scad files
|
||||
- Fuller PWA support w/ link Sharing, File opening / association to *.scad files...
|
||||
- Look into accessibility
|
||||
- Setup [OPENSCADPATH](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Libraries#Setting_OPENSCADPATH) env var w/ Emscripten to ensure examples that include assets / import local files will run fine.
|
||||
- Bundle more examples (ask users to contribute)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import React, { CSSProperties, useEffect, useState } from 'react';
|
||||
import {MultiLayoutComponentId, State} from '../state/app-state'
|
||||
import {MultiLayoutComponentId, State, StatePersister} from '../state/app-state'
|
||||
import { Model } from '../state/model';
|
||||
import EditorPanel from './EditorPanel';
|
||||
import ViewerPanel from './ViewerPanel';
|
||||
|
|
@ -13,10 +13,10 @@ import PanelSwitcher from './PanelSwitcher';
|
|||
// import "primereact/resources/primereact.min.css";
|
||||
// import "primeicons/primeicons.css";
|
||||
|
||||
export function App({initialState, fs}: {initialState: State, fs: FS}) {
|
||||
export function App({initialState, statePersister, fs}: {initialState: State, statePersister: StatePersister, fs: FS}) {
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
const model = new Model(fs, state, setState);
|
||||
const model = new Model(fs, state, setState, statePersister);
|
||||
useEffect(() => model.init());
|
||||
|
||||
const zIndexOfPanelsDependingOnFocus = {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { Button } from 'primereact/button';
|
|||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { Menu } from 'primereact/menu';
|
||||
import { ModelContext } from './contexts';
|
||||
import { isInStandaloneMode } from '../utils';
|
||||
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
|
||||
|
||||
export default function SettingsMenu({className, style}: {className?: string, style?: CSSProperties}) {
|
||||
const model = useContext(ModelContext);
|
||||
|
|
@ -44,7 +46,34 @@ export default function SettingsMenu({className, style}: {className?: string, st
|
|||
// disabled: true,
|
||||
command: () => model.mutate(s => s.view.lineNumbers = !s.view.lineNumbers)
|
||||
},
|
||||
...(isInStandaloneMode ? [
|
||||
{
|
||||
separator: true
|
||||
},
|
||||
{
|
||||
label: 'Clear local storage',
|
||||
icon: 'pi pi-list',
|
||||
// disabled: true,
|
||||
command: () => {
|
||||
confirmDialog({
|
||||
message: "This will clear all the edits you've made and files you've created in this playground " +
|
||||
"and will reset it to factory defaults. " +
|
||||
"Are you sure you wish to proceed? (you might lose your models!)",
|
||||
header: 'Clear local storage',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
accept: () => {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
},
|
||||
acceptLabel: `Clear all files!`,
|
||||
rejectLabel: 'Cancel'
|
||||
});
|
||||
},
|
||||
},
|
||||
] : []),
|
||||
] as MenuItem[]} popup ref={settingsMenu} />
|
||||
|
||||
<ConfirmDialog />
|
||||
<Button title="Settings menu"
|
||||
style={style}
|
||||
className={className}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ function configureAndInstallFS(windowOrSelf: Window, options: any) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function createEditorFS(prefix: string): Promise<FS> {
|
||||
export async function createEditorFS({prefix, allowPersistence}: {prefix: string, allowPersistence: boolean}): Promise<FS> {
|
||||
const archiveNames = deployedArchiveNames;
|
||||
const librariesMounts = await getBrowserFSLibrariesMounts(archiveNames);
|
||||
const allMounts: FSMounts = {};
|
||||
|
|
@ -103,7 +103,9 @@ export async function createEditorFS(prefix: string): Promise<FS> {
|
|||
...allMounts,
|
||||
}
|
||||
},
|
||||
writable: {
|
||||
writable: allowPersistence ? {
|
||||
fs: "LocalStorage",
|
||||
} : {
|
||||
fs: "InMemory"
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import { createInitialState } from './state/initial-state';
|
|||
import './index.css';
|
||||
|
||||
import debug from 'debug';
|
||||
import { registerCustomAppHeightCSSProperty } from './utils';
|
||||
import { isInStandaloneMode, registerCustomAppHeightCSSProperty } from './utils';
|
||||
import { State, StatePersister } from './state/app-state';
|
||||
import { writeStateInFragment } from "./state/fragment-state";
|
||||
|
||||
const log = debug('app:log');
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
|
|
@ -21,20 +24,48 @@ if (process.env.NODE_ENV !== 'production') {
|
|||
debug.disable();
|
||||
}
|
||||
|
||||
declare var BrowserFS: BrowserFSInterface
|
||||
|
||||
|
||||
(async () => {
|
||||
registerCustomAppHeightCSSProperty();
|
||||
|
||||
const fs = await createEditorFS('/');
|
||||
const fs = await createEditorFS({prefix: '/', allowPersistence: isInStandaloneMode});
|
||||
|
||||
await registerOpenSCADLanguage(fs, '/', zipArchives);
|
||||
|
||||
const initialState = createInitialState(fs, readStateFromFragment());
|
||||
let statePersister: StatePersister;
|
||||
let persistedState: State | null = null;
|
||||
|
||||
if (isInStandaloneMode) {
|
||||
const fs: FS = BrowserFS.BFSRequire('fs')
|
||||
try {
|
||||
const data = JSON.parse(new TextDecoder("utf-8").decode(fs.readFileSync('/state.json')));
|
||||
const {view, params} = data
|
||||
persistedState = {view, params};
|
||||
} catch (e) {
|
||||
console.log('Failed to read the persisted state from local storage.', e)
|
||||
}
|
||||
statePersister = {
|
||||
set: ({view, params}) => {
|
||||
fs.writeFile('/state.json', JSON.stringify({view, params}));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
persistedState = readStateFromFragment();
|
||||
statePersister = {
|
||||
set: writeStateInFragment,
|
||||
};
|
||||
}
|
||||
|
||||
const initialState = createInitialState(fs, persistedState);
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App initialState={initialState} fs={fs} />
|
||||
<App initialState={initialState} statePersister={statePersister} fs={fs} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ addEventListener('message', async (e) => {
|
|||
});
|
||||
|
||||
// This will mount lots of libraries' ZIP archives under /libraries/<name> -> <name>.zip
|
||||
await createEditorFS('');
|
||||
await createEditorFS({prefix: '', allowPersistence: false});
|
||||
|
||||
instance.FS.mkdir('/libraries');
|
||||
|
||||
|
|
|
|||
|
|
@ -50,4 +50,8 @@ export interface State {
|
|||
},
|
||||
};
|
||||
|
||||
export interface StatePersister {
|
||||
set(state: State): void;
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
@ -1,18 +1,13 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import { checkSyntax, render, RenderArgs, RenderOutput } from "../runner/actions";
|
||||
import { MultiLayoutComponentId, SingleLayoutComponentId, State } from "./app-state";
|
||||
import { MultiLayoutComponentId, SingleLayoutComponentId, State, StatePersister } from "./app-state";
|
||||
import { bubbleUpDeepMutations } from "./deep-mutate";
|
||||
import { writeStateInFragment } from "./fragment-state";
|
||||
import { formatBytes, formatMillis } from '../utils'
|
||||
import { getParentDir } from "../fs/filesystem";
|
||||
|
||||
// export const isFileWritable = (path: string) => !path.startsWith('/');
|
||||
// export const isFileWritable = (path: string) => getParentDir(path) === '/home';
|
||||
// export const isFileWritable = (path: string) => !path.startsWith('/libraries/');
|
||||
|
||||
export class Model {
|
||||
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void) {
|
||||
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
|
||||
private statePersister?: StatePersister) {
|
||||
}
|
||||
|
||||
init() {
|
||||
|
|
@ -24,7 +19,7 @@ export class Model {
|
|||
|
||||
private setState(state: State) {
|
||||
this.state = state;
|
||||
writeStateInFragment(state);
|
||||
this.statePersister && this.statePersister.set(state);
|
||||
this.setStateCallback && this.setStateCallback(state);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,4 +95,11 @@ export function registerCustomAppHeightCSSProperty() {
|
|||
}
|
||||
window.addEventListener('resize', updateAppHeight)
|
||||
updateAppHeight();
|
||||
}
|
||||
}
|
||||
|
||||
// In PWA mode, persist files in LocalStorage instead of the hash fragment.
|
||||
export const isInStandaloneMode =
|
||||
// true
|
||||
Boolean(('standalone' in window.navigator) && (window.navigator.standalone));
|
||||
|
||||
|
||||
Loading…
Reference in a new issue