Skip to content
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

Initial poc for Local File System #54

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"debug": "^4.3.4",
"file-system-access": "^1.0.4",
"monaco-editor": "^0.36.1",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",
Expand Down
47 changes: 26 additions & 21 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
// 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, StatePersister} 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';
import Footer from './Footer';
import { ModelContext, FSContext } from './contexts';
import { ModelContext, FSContext, FileSystemContext } from './contexts';
import PanelSwitcher from './PanelSwitcher';
import { ConfirmDialog } from 'primereact/confirmdialog';
import CustomizerPanel from './CustomizerPanel';
import { BaseFileSystem, DummyFileSystem, LocalStorage } from '../fs/base-filesystem';


// import "primereact/resources/themes/lara-light-indigo/theme.css";
// import "primereact/resources/primereact.min.css";
// import "primeicons/primeicons.css";

export function App({initialState, statePersister, fs}: {initialState: State, statePersister: StatePersister, 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, statePersister);
const [fileSystem, setFileSystem] = useState(new LocalStorage() as BaseFileSystem);

const model = new Model(fs, fileSystem, state, setState, statePersister);
useEffect(() => model.init());

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'F5') {
event.preventDefault();
model.render({isPreview: true, now: true})
model.render({ isPreview: true, now: true })
} else if (event.key === 'F6') {
event.preventDefault();
model.render({isPreview: false, now: true})
model.render({ isPreview: false, now: true })
}
};
window.addEventListener('keydown', handleKeyDown);
Expand Down Expand Up @@ -63,7 +65,7 @@ export function App({initialState, statePersister, fs}: {initialState: State, st
const itemCount = (layout.editor ? 1 : 0) + (layout.viewer ? 1 : 0) + (layout.customizer ? 1 : 0)
return {
flex: 1,
maxWidth: Math.floor(100/itemCount) + '%',
maxWidth: Math.floor(100 / itemCount) + '%',
display: (state.view.layout as any)[id] ? 'flex' : 'none'
}
} else {
Expand All @@ -74,37 +76,40 @@ export function App({initialState, statePersister, fs}: {initialState: State, st
}
}


return (
<ModelContext.Provider value={model}>
<FSContext.Provider value={fs}>
<div className='flex flex-column' style={{
<FileSystemContext.Provider value={{ fileSystem, setFileSystem }}>
<div className='flex flex-column' style={{
flex: 1,
}}>
<PanelSwitcher />
<div className={mode === 'multi' ? 'flex flex-row' : 'flex flex-column'}
style={mode === 'multi' ? {flex: 1} : {

<PanelSwitcher />

<div className={mode === 'multi' ? 'flex flex-row' : 'flex flex-column'}
style={mode === 'multi' ? { flex: 1 } : {
flex: 1,
position: 'relative'
}}>

<EditorPanel className={`
<EditorPanel className={`
opacity-animated
${layout.mode === 'single' && layout.focus !== 'editor' ? 'opacity-0' : ''}
${layout.mode === 'single' ? 'absolute-fill' : ''}
`} style={getPanelStyle('editor')} />
<ViewerPanel className={layout.mode === 'single' ? `absolute-fill` : ''} style={getPanelStyle('viewer')} />
<CustomizerPanel className={`
<ViewerPanel className={layout.mode === 'single' ? `absolute-fill` : ''} style={getPanelStyle('viewer')} />
<CustomizerPanel className={`
opacity-animated
${layout.mode === 'single' && layout.focus !== 'customizer' ? 'opacity-0' : ''}
${layout.mode === 'single' ? `absolute-fill` : ''}
`} style={getPanelStyle('customizer')} />
</div>
</div>

<Footer />
<ConfirmDialog />
</div>
<Footer />
<ConfirmDialog />
</div>
</FileSystemContext.Provider>
</FSContext.Provider>
</ModelContext.Provider>
);
Expand Down
111 changes: 63 additions & 48 deletions src/components/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { MenuItem } from 'primereact/menuitem';
import { Menu } from 'primereact/menu';
import { buildUrlForStateParams } from '../state/fragment-state';
import { blankProjectState, defaultSourcePath } from '../state/initial-state';
import { ModelContext, FSContext } from './contexts';
import FilePicker, { } from './FilePicker';
import { ModelContext, FSContext, FileSystemContext } from './contexts';
import FilePicker, { } from './FilePicker';
import { DummyFileSystem, LocalFileSystem } from '../fs/base-filesystem';

// const isMonacoSupported = false;
const isMonacoSupported = (() => {
Expand All @@ -25,11 +26,13 @@ if (isMonacoSupported) {
loader.init().then(mi => monacoInstance = mi);
}

export default function EditorPanel({className, style}: {className?: string, style?: CSSProperties}) {
export default function EditorPanel({ className, style }: { className?: string, style?: CSSProperties }) {

const model = useContext(ModelContext);
if (!model) throw new Error('No model');

const fileSystemState = useContext(FileSystemContext);

const menu = useRef<Menu>(null);

const state = model.state;
Expand All @@ -50,12 +53,12 @@ export default function EditorPanel({className, style}: {className?: string, sty
editor.addAction({
id: "openscad-render",
label: "Render OpenSCAD",
run: () => model.render({isPreview: false, now: true})
run: () => model.render({ isPreview: false, now: true })
});
editor.addAction({
id: "openscad-preview",
label: "Preview OpenSCAD",
run: () => model.render({isPreview: true, now: true})
run: () => model.render({ isPreview: true, now: true })
});
setEditor(editor)
}
Expand All @@ -72,7 +75,7 @@ export default function EditorPanel({className, style}: {className?: string, sty
<div className='flex flex-row gap-2' style={{
margin: '5px',
}}>

<Menu model={[
{
label: "New project",
Expand All @@ -89,12 +92,19 @@ export default function EditorPanel({className, style}: {className?: string, sty
},
{
separator: true
},
},
{
// TODO: popup to ask for file name
label: "New file",
icon: 'pi pi-plus',
disabled: true,
disabled: false,
command: async () => {
const name = window.prompt("New file name", "example.scad");
if (name !== null) {
await fileSystemState?.fileSystem.createFile(name);
await model.openFile(name);
}
},
},
{
label: "Copy to new file",
Expand All @@ -114,6 +124,11 @@ export default function EditorPanel({className, style}: {className?: string, sty
{
separator: true
},
{
label: 'Use Local File System',
disabled: !('showOpenFilePicker' in self),
command: () => { const fileSystem = new LocalFileSystem(); fileSystem.initialise().finally(() => fileSystemState?.setFileSystem(fileSystem)) },
},
// https://vscode-docs.readthedocs.io/en/stable/customization/keybindings/
// {
// label: 'Undo',
Expand All @@ -137,49 +152,49 @@ export default function EditorPanel({className, style}: {className?: string, sty
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardCopyAction', null),
// },
// {
// label: 'Cut',
// icon: 'pi pi-eraser',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardCutAction', null),
// },
// {
// label: 'Paste',
// icon: 'pi pi-images',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardPasteAction', null),
// },
{
label: 'Select All',
icon: 'pi pi-info-circle',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'editor.action.selectAll', null),
},
{
separator: true
},
{
label: 'Find',
icon: 'pi pi-search',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'actions.find', null),
},
// label: 'Cut',
// icon: 'pi pi-eraser',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardCutAction', null),
// },
// {
// label: 'Paste',
// icon: 'pi pi-images',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardPasteAction', null),
// },
{
label: 'Select All',
icon: 'pi pi-info-circle',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'editor.action.selectAll', null),
},
{
separator: true
},
{
label: 'Find',
icon: 'pi pi-search',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'actions.find', null),
},
] as MenuItem[]} popup ref={menu} />
<Button title="Editor menu" rounded text icon="pi pi-ellipsis-h" onClick={(e) => menu.current && menu.current.toggle(e)} />

<FilePicker
style={{
flex: 1,
}}/>

{state.params.sourcePath !== defaultSourcePath &&
<Button icon="pi pi-chevron-left"
text
onClick={() => model.openFile(defaultSourcePath)}
title={`Go back to ${defaultSourcePath}`}/>}
<FilePicker
style={{
flex: 1,
}} />

{state.params.sourcePath !== defaultSourcePath &&
<Button icon="pi pi-chevron-left"
text
onClick={() => model.openFile(defaultSourcePath)}
title={`Go back to ${defaultSourcePath}`} />}

</div>


<div style={{
position: 'relative',
flex: 1
Expand All @@ -201,12 +216,12 @@ export default function EditorPanel({className, style}: {className?: string, sty
/>
)}
{!isMonacoSupported && (
<InputTextarea
<InputTextarea
style={{
}}
className="openscad-editor absolute-fill"
value={state.params.source}
onChange={s => model.source = s.target.value ?? ''}
onChange={s => model.source = s.target.value ?? ''}
/>
)}
</div>
Expand All @@ -219,7 +234,7 @@ export default function EditorPanel({className, style}: {className?: string, sty
<pre><code id="logs" style={{
}}>{state.lastCheckerRun?.logText ?? 'No log yet!'}</code></pre>
</div>

</div>
)
}
60 changes: 36 additions & 24 deletions src/components/FilePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.

import { CSSProperties, useContext } from 'react';
import { CSSProperties, useContext, useEffect, useState } from 'react';
import { TreeSelect } from 'primereact/treeselect';
import TreeNode from 'primereact/treenode';
import { ModelContext, FSContext } from './contexts';
import { ModelContext, FSContext, FileSystemContext } from './contexts';
// import { isFileWritable } from '../state/model';
import { join } from '../fs/filesystem';
import { defaultSourcePath } from '../state/initial-state';
import { zipArchives } from '../fs/zip-archives';

const biasedCompare = (a: string, b: string) =>
const biasedCompare = (a: string, b: string) =>
a === 'openscad' ? -1 : b === 'openscad' ? 1 : a.localeCompare(b);

function listFilesAsNodes(fs: FS, path: string, accept?: (path: string) => boolean): TreeNode[] {
Expand Down Expand Up @@ -82,34 +82,46 @@ function listFilesAsNodes(fs: FS, path: string, accept?: (path: string) => boole
return nodes;
}

export default function FilePicker({className, style}: {className?: string, style?: CSSProperties}) {
export default function FilePicker({ className, style }: { className?: string, style?: CSSProperties }) {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const state = model.state;

const fs = useContext(FSContext);

const fsItems = fs && listFilesAsNodes(fs, '/')
const fileSystem = useContext(FileSystemContext);

const [files, setFiles] = useState<TreeNode[]>([]);

useEffect(() => {
async function apiCall() {
const apiResponse = await fileSystem?.fileSystem.getFiles();
console.log(apiResponse);
// @ts-ignore
setFiles(apiResponse);
}
apiCall();
}, [files]);

return (
<TreeSelect
className={className}
title='OpenSCAD Playground Files'
value={state.params.sourcePath}
resetFilterOnHide={true}
filterBy="key"
onChange={e => {
const key = e.value;
if (typeof key === 'string') {
if (key.startsWith('https://')) {
window.open(key, '_blank')
} else {
model.openFile(key);
}
}
}}
filter
style={style}
options={fsItems} />
<TreeSelect
className={className}
title='OpenSCAD Playground Files'
value={state.params.sourcePath}
resetFilterOnHide={true}
filterBy="key"
onChange={e => {
const key = e.value;
if (typeof key === 'string') {
if (key.startsWith('https://')) {
window.open(key, '_blank')
} else {
model.openFile(key);
}
}
}}
filter
style={style}
options={files} />
)
}
Loading