Skip to content

Commit edc0225

Browse files
authored
Merge pull request #282 from GBSL-Informatik/feature/excalidoc-standalone-editor
Feature/excalidoc-standalone-editor
2 parents 61b0b88 + 170c9f3 commit edc0225

18 files changed

Lines changed: 987 additions & 116 deletions

packages/tdev/excalidoc/ImageMarkupEditor/EditorPopup/index.tsx

Lines changed: 10 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,8 @@ import { SIZE_S } from '@tdev-components/shared/iconSizes';
1010
import { mdiClose, mdiImageEditOutline } from '@mdi/js';
1111
import ImageMarkupEditor from '..';
1212
import requestDocusaurusRootAcess from '@tdev-components/util/localFS/requestDocusaurusRootAcess';
13-
import requestFileHandle from '@tdev-components/util/localFS/requestFileHandle';
14-
import { createExcalidrawMarkup, updateRectangleDimensions } from '../helpers/createExcalidrawMarkup';
15-
import type { ExcalidrawInitialDataState } from '@excalidraw/excalidraw/types';
1613
import type { PopupActions } from 'reactjs-popup/dist/types';
17-
import extractExalidrawImageName from '../helpers/extractExalidrawImageName';
18-
import dataUrlToBlob from '../helpers/dataUrlToBlob';
19-
import { getImageElementFromScene, getImageFileFromScene } from '../helpers/getElementsFromScene';
20-
import type { OrderedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
21-
import { CustomData } from '../helpers/constants';
14+
import useExcalidrawSource from '../hooks/useExcalidrawSource';
2215

2316
interface Props {
2417
src: string;
@@ -28,11 +21,11 @@ interface Props {
2821
const EditorPopup = observer((props: Props) => {
2922
const sessionStore = useStore('sessionStore');
3023
const ref = React.useRef<PopupActions>(null);
31-
const { excaliName, excaliSrc, imgName, mimeType } = React.useMemo(
32-
() => extractExalidrawImageName(props.src),
33-
[props.src]
24+
const root = sessionStore.fileSystemDirectoryHandles.get('root') || null;
25+
const { excaliState, setExcaliState, mimeType, load, save, restore } = useExcalidrawSource(
26+
root,
27+
props.src
3428
);
35-
const [excaliState, setExcaliState] = React.useState<ExcalidrawInitialDataState | null>(null);
3629

3730
return (
3831
<Popup
@@ -53,38 +46,11 @@ const EditorPopup = observer((props: Props) => {
5346
return;
5447
}
5548
}
56-
const root = sessionStore.fileSystemDirectoryHandles.get('root');
57-
if (!root) {
49+
if (!sessionStore.fileSystemDirectoryHandles.get('root')) {
5850
window.alert('Kein Zugriff auf lokale Dateien. Bitte aktiviere den Zugriff.');
5951
return;
6052
}
61-
try {
62-
let fileHandle: FileSystemFileHandle;
63-
let parentDir: FileSystemDirectoryHandle;
64-
try {
65-
({ fileHandle, parentDir } = await requestFileHandle(root, excaliSrc, 'readwrite'));
66-
} catch (error) {
67-
// If the file does not exist, create a new one
68-
({ fileHandle, parentDir } = await requestFileHandle(root, props.src, 'read'));
69-
const excaliData = await createExcalidrawMarkup(fileHandle);
70-
const excaliFile = await parentDir.getFileHandle(excaliName, { create: true });
71-
await excaliFile.createWritable().then(async (writable) => {
72-
await writable.write(JSON.stringify(excaliData, null, 2));
73-
await writable.close();
74-
});
75-
fileHandle = excaliFile;
76-
}
77-
const data = await fileHandle
78-
.getFile()
79-
.then((content) => {
80-
return content.text();
81-
})
82-
.then((text) => JSON.parse(text) as ExcalidrawInitialDataState);
83-
setExcaliState(updateRectangleDimensions(data));
84-
} catch (error) {
85-
console.error('Error processing image:', error);
86-
window.alert(`Error processing image: ${error}`);
87-
}
53+
await load();
8854
}}
8955
onClose={() => {
9056
setExcaliState(null);
@@ -117,83 +83,12 @@ const EditorPopup = observer((props: Props) => {
11783
ref.current?.close();
11884
}}
11985
onSave={async (state, blob, asWebp) => {
120-
const root = sessionStore.fileSystemDirectoryHandles.get('root');
121-
let exaliExport = excaliSrc;
122-
let imgExport = props.src;
123-
const needsTransform = asWebp && !/\.webp$/i.test(props.src);
124-
if (needsTransform) {
125-
exaliExport = exaliExport.replace(
126-
`${imgName}.excalidraw`,
127-
`${imgName.split('.').slice(0, -1).join('.')}.webp.excalidraw`
128-
);
129-
imgExport = imgExport.replace(
130-
`${imgName}`,
131-
`${imgName.split('.').slice(0, -1).join('.')}.webp`
132-
);
133-
}
134-
135-
const { fileHandle, parentDir } = await requestFileHandle(
136-
root!,
137-
exaliExport,
138-
'readwrite',
139-
true
140-
);
141-
const { fileHandle: imgHandle } = await requestFileHandle(
142-
root!,
143-
imgExport,
144-
'readwrite',
145-
true
146-
);
147-
await fileHandle.createWritable().then(async (writable) => {
148-
await writable.write(JSON.stringify(state, null, 2));
149-
await writable.close();
150-
});
151-
await imgHandle.createWritable().then(async (writable) => {
152-
await writable.write(blob);
153-
await writable.close();
154-
});
155-
if (needsTransform) {
156-
try {
157-
await parentDir.removeEntry(imgName);
158-
await parentDir.removeEntry(`${imgName}.excalidraw`);
159-
} catch (err) {
160-
console.error(`Error removing entry when transforming to WebP:`, err);
161-
}
162-
}
86+
await save(state, blob, asWebp);
16387
ref.current?.close();
16488
}}
16589
onRestore={async () => {
166-
const root = sessionStore.fileSystemDirectoryHandles.get('root');
167-
const { fileHandle, parentDir } = await requestFileHandle(
168-
root!,
169-
excaliSrc,
170-
'read'
171-
);
172-
const data = await fileHandle
173-
.getFile()
174-
.then((content) => content.text())
175-
.then((text) => JSON.parse(text) as ExcalidrawInitialDataState);
176-
const [backgroundImage] = getImageElementFromScene(
177-
data.elements as readonly OrderedExcalidrawElement[]
178-
);
179-
const backgroundFile = getImageFileFromScene(data.files);
180-
if (backgroundFile && backgroundImage) {
181-
const data = backgroundImage.customData as Partial<CustomData>;
182-
const initExtension = data.initExtension || '.png';
183-
const restoredName = imgName.endsWith(initExtension)
184-
? imgName
185-
: `${imgName.split('.').slice(0, -1).join('.')}${initExtension}`;
186-
const imgHandle = await parentDir.getFileHandle(restoredName, {
187-
create: true
188-
});
189-
await imgHandle.createWritable().then(async (writable) => {
190-
await writable.write(dataUrlToBlob(backgroundFile.dataURL));
191-
await writable.close();
192-
});
193-
await parentDir.removeEntry(excaliName);
194-
if (restoredName !== imgName) {
195-
await parentDir.removeEntry(imgName);
196-
}
90+
const restored = await restore();
91+
if (restored) {
19792
ref.current?.close();
19893
}
19994
}}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react';
2+
import clsx from 'clsx';
3+
import styles from './styles.module.scss';
4+
import Button from '@tdev-components/shared/Button';
5+
import Icon from '@mdi/react';
6+
import {
7+
mdiFolderOpen,
8+
mdiChevronLeft,
9+
mdiChevronRight,
10+
mdiFilePlusOutline,
11+
mdiRenameOutline
12+
} from '@mdi/js';
13+
import Dir, { DirType } from '@tdev-components/FileSystem/Dir';
14+
import RequestFullscreen from '@tdev-components/shared/RequestFullscreen';
15+
16+
interface Props {
17+
fullscreenTargetId: string;
18+
dirHandle: FileSystemDirectoryHandle | null;
19+
dirTree: DirType | null;
20+
selectedSrc: string | null;
21+
onSelectFolder: () => void;
22+
onSelect: (fName?: string) => void;
23+
onCreateNewDrawing: () => void;
24+
onRenameImage: () => void;
25+
}
26+
27+
const DesktopSidebar = (props: Props) => {
28+
const {
29+
fullscreenTargetId,
30+
dirHandle,
31+
dirTree,
32+
selectedSrc,
33+
onSelectFolder,
34+
onSelect,
35+
onCreateNewDrawing,
36+
onRenameImage
37+
} = props;
38+
const [collapsed, setCollapsed] = React.useState(false);
39+
40+
return (
41+
<div className={clsx(styles.sidebar, collapsed && styles.collapsed)}>
42+
<div className={clsx(styles.sidebarHeader)}>
43+
{!collapsed && (
44+
<>
45+
<Button
46+
icon={mdiFolderOpen}
47+
title="Ordner auswählen"
48+
onClick={onSelectFolder}
49+
color="primary"
50+
/>
51+
{dirHandle && (
52+
<Button
53+
icon={mdiFilePlusOutline}
54+
title="Neue Zeichnung erstellen"
55+
onClick={onCreateNewDrawing}
56+
color="primary"
57+
/>
58+
)}
59+
{selectedSrc && (
60+
<Button
61+
icon={mdiRenameOutline}
62+
title="Bild umbenennen"
63+
onClick={onRenameImage}
64+
color="primary"
65+
/>
66+
)}
67+
</>
68+
)}
69+
{!collapsed && <RequestFullscreen targetId={fullscreenTargetId} />}
70+
<button
71+
className={clsx(styles.collapseToggle)}
72+
onClick={() => setCollapsed((prev) => !prev)}
73+
title={collapsed ? 'Dateiliste einblenden' : 'Dateiliste ausblenden'}
74+
>
75+
<Icon path={collapsed ? mdiChevronRight : mdiChevronLeft} size={0.8} />
76+
</button>
77+
</div>
78+
{!collapsed && dirTree && (
79+
<div className={clsx(styles.fileTree)}>
80+
<Dir
81+
dir={dirTree}
82+
open={2}
83+
path={selectedSrc ? `${dirTree.name}/${selectedSrc}` : undefined}
84+
onSelect={onSelect}
85+
/>
86+
</div>
87+
)}
88+
</div>
89+
);
90+
};
91+
92+
export default DesktopSidebar;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react';
2+
import clsx from 'clsx';
3+
import styles from './styles.module.scss';
4+
import Button from '@tdev-components/shared/Button';
5+
import Icon from '@mdi/react';
6+
import { mdiFolderOpen, mdiFileTree, mdiClose, mdiFilePlusOutline, mdiRenameOutline } from '@mdi/js';
7+
import Dir, { DirType } from '@tdev-components/FileSystem/Dir';
8+
import RequestFullscreen from '@tdev-components/shared/RequestFullscreen';
9+
10+
interface Props {
11+
fullscreenTargetId: string;
12+
dirHandle: FileSystemDirectoryHandle | null;
13+
dirTree: DirType | null;
14+
selectedSrc: string | null;
15+
onSelectFolder: () => void;
16+
onSelect: (fName?: string) => void;
17+
onCreateNewDrawing: () => void;
18+
onRenameImage: () => void;
19+
}
20+
21+
const MobileSidebar = (props: Props) => {
22+
const {
23+
fullscreenTargetId,
24+
dirHandle,
25+
dirTree,
26+
selectedSrc,
27+
onSelectFolder,
28+
onSelect,
29+
onCreateNewDrawing,
30+
onRenameImage
31+
} = props;
32+
const [treeOpen, setTreeOpen] = React.useState(false);
33+
34+
const onMobileSelect = React.useCallback(
35+
(fName?: string) => {
36+
onSelect(fName);
37+
setTreeOpen(false);
38+
},
39+
[onSelect]
40+
);
41+
42+
return (
43+
<>
44+
{treeOpen && (
45+
<div className={clsx(styles.mobileOverlay)}>
46+
<div className={clsx(styles.mobileOverlayHeader)}>
47+
<Button
48+
icon={mdiFolderOpen}
49+
title="Ordner auswählen"
50+
onClick={onSelectFolder}
51+
color="primary"
52+
/>
53+
{dirHandle && (
54+
<Button
55+
icon={mdiFilePlusOutline}
56+
title="Neue Zeichnung erstellen"
57+
onClick={onCreateNewDrawing}
58+
color="primary"
59+
/>
60+
)}
61+
{selectedSrc && (
62+
<Button
63+
icon={mdiRenameOutline}
64+
title="Bild umbenennen"
65+
onClick={onRenameImage}
66+
color="primary"
67+
/>
68+
)}
69+
<button
70+
className={clsx(styles.collapseToggle)}
71+
onClick={() => setTreeOpen(false)}
72+
title="Dateiliste schliessen"
73+
>
74+
<Icon path={mdiClose} size={0.8} />
75+
</button>
76+
</div>
77+
{dirTree && (
78+
<div className={clsx(styles.fileTree)}>
79+
<Dir
80+
dir={dirTree}
81+
open={2}
82+
path={selectedSrc ? `${dirTree.name}/${selectedSrc}` : undefined}
83+
onSelect={onMobileSelect}
84+
/>
85+
</div>
86+
)}
87+
</div>
88+
)}
89+
<div className={clsx(styles.mobileToolbar)}>
90+
<button
91+
className={clsx(styles.collapseToggle)}
92+
onClick={() => setTreeOpen(true)}
93+
title="Dateiliste anzeigen"
94+
>
95+
<Icon path={mdiFileTree} size={0.8} />
96+
</button>
97+
{!dirHandle && (
98+
<Button
99+
icon={mdiFolderOpen}
100+
text="Ordner"
101+
title="Ordner auswählen"
102+
onClick={onSelectFolder}
103+
color="primary"
104+
/>
105+
)}
106+
<RequestFullscreen targetId={fullscreenTargetId} />
107+
</div>
108+
</>
109+
);
110+
};
111+
112+
export default MobileSidebar;

0 commit comments

Comments
 (0)