aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2024-02-23 19:49:30 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2024-02-23 19:49:30 +0100
commit4944ca0f5ab9adfc17a45fadc778c7b78568a038 (patch)
treeacabe2d42592a422b6032078e3114c0da6b7a2fa
parentfix(frontend): top button styling (diff)
downloadrefinery-4944ca0f5ab9adfc17a45fadc778c7b78568a038.tar.gz
refinery-4944ca0f5ab9adfc17a45fadc778c7b78568a038.tar.zst
refinery-4944ca0f5ab9adfc17a45fadc778c7b78568a038.zip
refactor(web): use filesystem access API when available
-rw-r--r--subprojects/frontend/src/graph/exportDiagram.tsx35
-rw-r--r--subprojects/frontend/src/utils/fileIO.ts63
-rw-r--r--subprojects/frontend/types/filesystemAccess.d.ts23
3 files changed, 92 insertions, 29 deletions
diff --git a/subprojects/frontend/src/graph/exportDiagram.tsx b/subprojects/frontend/src/graph/exportDiagram.tsx
index 46c7f199..3ba278f9 100644
--- a/subprojects/frontend/src/graph/exportDiagram.tsx
+++ b/subprojects/frontend/src/graph/exportDiagram.tsx
@@ -18,6 +18,7 @@ import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw';
18import type { Theme } from '@mui/material/styles'; 18import type { Theme } from '@mui/material/styles';
19 19
20import { darkTheme, lightTheme } from '../theme/ThemeProvider'; 20import { darkTheme, lightTheme } from '../theme/ThemeProvider';
21import { copyBlob, saveBlob } from '../utils/fileIO';
21 22
22import type ExportSettingsStore from './ExportSettingsStore'; 23import type ExportSettingsStore from './ExportSettingsStore';
23import type GraphStore from './GraphStore'; 24import type GraphStore from './GraphStore';
@@ -25,7 +26,9 @@ import { createGraphTheme } from './GraphTheme';
25import { SVG_NS } from './postProcessSVG'; 26import { SVG_NS } from './postProcessSVG';
26 27
27const PROLOG = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'; 28const PROLOG = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>';
29const PNG_CONTENT_TYPE = 'image/png';
28const SVG_CONTENT_TYPE = 'image/svg+xml'; 30const SVG_CONTENT_TYPE = 'image/svg+xml';
31const EXPORT_ID = 'export-image';
29 32
30const ICONS: Map<string, Element> = new Map(); 33const ICONS: Map<string, Element> = new Map();
31 34
@@ -213,32 +216,6 @@ function serializeSVG(svgDocument: XMLDocument): Blob {
213 }); 216 });
214} 217}
215 218
216function downloadBlob(blob: Blob, name: string): void {
217 const link = document.createElement('a');
218 const url = window.URL.createObjectURL(blob);
219 try {
220 link.href = url;
221 link.download = name;
222 link.style.display = 'none';
223 document.body.appendChild(link);
224 link.click();
225 } finally {
226 window.URL.revokeObjectURL(url);
227 document.body.removeChild(link);
228 }
229}
230
231async function copyBlob(blob: Blob): Promise<void> {
232 const { clipboard } = navigator;
233 if ('write' in clipboard) {
234 await clipboard.write([
235 new ClipboardItem({
236 [blob.type]: blob,
237 }),
238 ]);
239 }
240}
241
242async function serializePNG( 219async function serializePNG(
243 serializedSVG: Blob, 220 serializedSVG: Blob,
244 svg: SVGSVGElement, 221 svg: SVGSVGElement,
@@ -302,7 +279,7 @@ async function serializePNG(
302 } else { 279 } else {
303 resolve(exportedBlob); 280 resolve(exportedBlob);
304 } 281 }
305 }, 'image/png'); 282 }, PNG_CONTENT_TYPE);
306 }); 283 });
307} 284}
308 285
@@ -353,11 +330,11 @@ export default async function exportDiagram(
353 if (mode === 'copy') { 330 if (mode === 'copy') {
354 await copyBlob(png); 331 await copyBlob(png);
355 } else { 332 } else {
356 downloadBlob(png, 'graph.png'); 333 await saveBlob(png, 'graph.png', PNG_CONTENT_TYPE, EXPORT_ID);
357 } 334 }
358 } else if (mode === 'copy') { 335 } else if (mode === 'copy') {
359 await copyBlob(serializedSVG); 336 await copyBlob(serializedSVG);
360 } else { 337 } else {
361 downloadBlob(serializedSVG, 'graph.svg'); 338 await saveBlob(serializedSVG, 'graph.svg', SVG_CONTENT_TYPE, EXPORT_ID);
362 } 339 }
363} 340}
diff --git a/subprojects/frontend/src/utils/fileIO.ts b/subprojects/frontend/src/utils/fileIO.ts
new file mode 100644
index 00000000..abcc43eb
--- /dev/null
+++ b/subprojects/frontend/src/utils/fileIO.ts
@@ -0,0 +1,63 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7export async function saveBlob(
8 blob: Blob,
9 name: string,
10 mimeType: string,
11 id?: string,
12): Promise<void> {
13 if ('showSaveFilePicker' in window) {
14 const options: FilePickerOptions = {
15 suggestedName: name,
16 };
17 if (id !== undefined) {
18 options.id = id;
19 }
20 const extensionIndex = name.lastIndexOf('.');
21 if (extensionIndex >= 0) {
22 options.types = [
23 {
24 description: `${name.substring(extensionIndex + 1)} files`,
25 accept: {
26 [mimeType]: [name.substring(extensionIndex)],
27 },
28 },
29 ];
30 }
31 const handle = await window.showSaveFilePicker(options);
32 const writable = await handle.createWritable();
33 try {
34 await writable.write(blob);
35 } finally {
36 await writable.close();
37 }
38 return;
39 }
40 const link = document.createElement('a');
41 const url = window.URL.createObjectURL(blob);
42 try {
43 link.href = url;
44 link.download = name;
45 link.style.display = 'none';
46 document.body.appendChild(link);
47 link.click();
48 } finally {
49 window.URL.revokeObjectURL(url);
50 document.body.removeChild(link);
51 }
52}
53
54export async function copyBlob(blob: Blob): Promise<void> {
55 const { clipboard } = navigator;
56 if ('write' in clipboard) {
57 await clipboard.write([
58 new ClipboardItem({
59 [blob.type]: blob,
60 }),
61 ]);
62 }
63}
diff --git a/subprojects/frontend/types/filesystemAccess.d.ts b/subprojects/frontend/types/filesystemAccess.d.ts
new file mode 100644
index 00000000..000cd2a5
--- /dev/null
+++ b/subprojects/frontend/types/filesystemAccess.d.ts
@@ -0,0 +1,23 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7interface FilePickerOptions {
8 suggestedName?: string;
9 id?: string;
10 types?: {
11 description?: string;
12 accept: Record<string, string[]>;
13 }[];
14}
15
16interface Window {
17 showOpenFilePicker?: (
18 options?: FilePickerOptions,
19 ) => Promise<FileSystemFileHandle>;
20 showSaveFilePicker?: (
21 options?: FilePickerOptions,
22 ) => Promise<FileSystemFileHandle>;
23}