diff options
Diffstat (limited to 'subprojects/frontend/src/graph/ExportPanel.tsx')
-rw-r--r-- | subprojects/frontend/src/graph/ExportPanel.tsx | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/subprojects/frontend/src/graph/ExportPanel.tsx b/subprojects/frontend/src/graph/ExportPanel.tsx new file mode 100644 index 00000000..2621deff --- /dev/null +++ b/subprojects/frontend/src/graph/ExportPanel.tsx | |||
@@ -0,0 +1,215 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import ChevronRightIcon from '@mui/icons-material/ChevronRight'; | ||
8 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | ||
9 | import DarkModeIcon from '@mui/icons-material/DarkMode'; | ||
10 | import ImageIcon from '@mui/icons-material/Image'; | ||
11 | import LightModeIcon from '@mui/icons-material/LightMode'; | ||
12 | import SaveAltIcon from '@mui/icons-material/SaveAlt'; | ||
13 | import ShapeLineIcon from '@mui/icons-material/ShapeLine'; | ||
14 | import Box from '@mui/material/Box'; | ||
15 | import Button from '@mui/material/Button'; | ||
16 | import FormControlLabel from '@mui/material/FormControlLabel'; | ||
17 | import Slider from '@mui/material/Slider'; | ||
18 | import Stack from '@mui/material/Stack'; | ||
19 | import Switch from '@mui/material/Switch'; | ||
20 | import ToggleButton from '@mui/material/ToggleButton'; | ||
21 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; | ||
22 | import Typography from '@mui/material/Typography'; | ||
23 | import { styled } from '@mui/material/styles'; | ||
24 | import { observer } from 'mobx-react-lite'; | ||
25 | import { useCallback } from 'react'; | ||
26 | |||
27 | import { useRootStore } from '../RootStoreProvider'; | ||
28 | import getLogger from '../utils/getLogger'; | ||
29 | |||
30 | import type GraphStore from './GraphStore'; | ||
31 | import SlideInPanel from './SlideInPanel'; | ||
32 | import exportDiagram from './exportDiagram'; | ||
33 | |||
34 | const log = getLogger('graph.ExportPanel'); | ||
35 | |||
36 | const SwitchButtonGroup = styled(ToggleButtonGroup, { | ||
37 | name: 'ExportPanel-SwitchButtonGroup', | ||
38 | })(({ theme }) => ({ | ||
39 | marginTop: theme.spacing(2), | ||
40 | marginInline: theme.spacing(2), | ||
41 | minWidth: '260px', | ||
42 | '.MuiToggleButton-root': { | ||
43 | width: '100%', | ||
44 | fontSize: '1rem', | ||
45 | lineHeight: '1.5', | ||
46 | }, | ||
47 | '& svg': { | ||
48 | margin: '0 6px 0 0', | ||
49 | }, | ||
50 | })); | ||
51 | |||
52 | function getLabel(value: number): string { | ||
53 | return `${value}%`; | ||
54 | } | ||
55 | |||
56 | const marks = [100, 200, 300, 400].map((value) => ({ | ||
57 | value, | ||
58 | label: ( | ||
59 | <Stack direction="column" alignItems="center"> | ||
60 | <ImageIcon sx={{ width: `${11 + (value / 100) * 3}px` }} /> | ||
61 | <Typography variant="caption">{getLabel(value)}</Typography> | ||
62 | </Stack> | ||
63 | ), | ||
64 | })); | ||
65 | |||
66 | function ExportPanel({ | ||
67 | graph, | ||
68 | svgContainer, | ||
69 | dialog, | ||
70 | }: { | ||
71 | graph: GraphStore; | ||
72 | svgContainer: HTMLElement | undefined; | ||
73 | dialog: boolean; | ||
74 | }): JSX.Element { | ||
75 | const { exportSettingsStore } = useRootStore(); | ||
76 | |||
77 | const icon = useCallback( | ||
78 | (show: boolean) => | ||
79 | show && !dialog ? <ChevronRightIcon /> : <SaveAltIcon />, | ||
80 | [dialog], | ||
81 | ); | ||
82 | |||
83 | const { format } = exportSettingsStore; | ||
84 | const emptyGraph = graph.semantics.nodes.length === 0; | ||
85 | const buttons = useCallback( | ||
86 | (close: () => void) => ( | ||
87 | <> | ||
88 | <Button | ||
89 | color="inherit" | ||
90 | startIcon={<SaveAltIcon />} | ||
91 | disabled={emptyGraph} | ||
92 | onClick={() => { | ||
93 | exportDiagram(svgContainer, graph, exportSettingsStore, 'download') | ||
94 | .then(close) | ||
95 | .catch((error) => { | ||
96 | log.error('Failed to download diagram', error); | ||
97 | }); | ||
98 | }} | ||
99 | > | ||
100 | Download | ||
101 | </Button> | ||
102 | {'write' in navigator.clipboard && format === 'png' && ( | ||
103 | <Button | ||
104 | color="inherit" | ||
105 | startIcon={<ContentCopyIcon />} | ||
106 | disabled={emptyGraph} | ||
107 | onClick={() => { | ||
108 | exportDiagram(svgContainer, graph, exportSettingsStore, 'copy') | ||
109 | .then(close) | ||
110 | .catch((error) => { | ||
111 | log.error('Failed to copy diagram', error); | ||
112 | }); | ||
113 | }} | ||
114 | > | ||
115 | Copy | ||
116 | </Button> | ||
117 | )} | ||
118 | </> | ||
119 | ), | ||
120 | [svgContainer, graph, exportSettingsStore, format, emptyGraph], | ||
121 | ); | ||
122 | |||
123 | return ( | ||
124 | <SlideInPanel | ||
125 | anchor="right" | ||
126 | dialog={dialog} | ||
127 | title="Export diagram" | ||
128 | icon={icon} | ||
129 | iconLabel="Show export panel" | ||
130 | buttons={buttons} | ||
131 | > | ||
132 | <SwitchButtonGroup size="small" className="rounded"> | ||
133 | <ToggleButton | ||
134 | value="svg" | ||
135 | selected={exportSettingsStore.format === 'svg'} | ||
136 | onClick={() => exportSettingsStore.setFormat('svg')} | ||
137 | > | ||
138 | <ShapeLineIcon fontSize="small" /> SVG | ||
139 | </ToggleButton> | ||
140 | <ToggleButton | ||
141 | value="png" | ||
142 | selected={exportSettingsStore.format === 'png'} | ||
143 | onClick={() => exportSettingsStore.setFormat('png')} | ||
144 | > | ||
145 | <ImageIcon fontSize="small" /> PNG | ||
146 | </ToggleButton> | ||
147 | </SwitchButtonGroup> | ||
148 | <SwitchButtonGroup size="small" className="rounded"> | ||
149 | <ToggleButton | ||
150 | value="svg" | ||
151 | selected={exportSettingsStore.theme === 'light'} | ||
152 | onClick={() => exportSettingsStore.setTheme('light')} | ||
153 | > | ||
154 | <LightModeIcon fontSize="small" /> Light | ||
155 | </ToggleButton> | ||
156 | <ToggleButton | ||
157 | value="png" | ||
158 | selected={exportSettingsStore.theme === 'dark'} | ||
159 | onClick={() => exportSettingsStore.setTheme('dark')} | ||
160 | > | ||
161 | <DarkModeIcon fontSize="small" /> Dark | ||
162 | </ToggleButton> | ||
163 | </SwitchButtonGroup> | ||
164 | <FormControlLabel | ||
165 | control={ | ||
166 | <Switch | ||
167 | checked={exportSettingsStore.transparent} | ||
168 | onClick={() => exportSettingsStore.toggleTransparent()} | ||
169 | /> | ||
170 | } | ||
171 | label="Transparent background" | ||
172 | /> | ||
173 | {exportSettingsStore.format === 'svg' && ( | ||
174 | <FormControlLabel | ||
175 | control={ | ||
176 | <Switch | ||
177 | checked={exportSettingsStore.embedFonts} | ||
178 | onClick={() => exportSettingsStore.toggleEmbedFonts()} | ||
179 | /> | ||
180 | } | ||
181 | label={ | ||
182 | <Stack direction="column"> | ||
183 | <Typography>Embed fonts</Typography> | ||
184 | <Typography variant="caption"> | ||
185 | +75 kB, only supported in browsers | ||
186 | </Typography> | ||
187 | </Stack> | ||
188 | } | ||
189 | /> | ||
190 | )} | ||
191 | {exportSettingsStore.format === 'png' && ( | ||
192 | <Box mx={4} mt={1} mb={2}> | ||
193 | <Slider | ||
194 | aria-label="Image scale" | ||
195 | value={exportSettingsStore.scale} | ||
196 | min={100} | ||
197 | max={400} | ||
198 | valueLabelFormat={getLabel} | ||
199 | getAriaValueText={getLabel} | ||
200 | step={50} | ||
201 | valueLabelDisplay="auto" | ||
202 | marks={marks} | ||
203 | onChange={(_, value) => { | ||
204 | if (typeof value === 'number') { | ||
205 | exportSettingsStore.setScale(value); | ||
206 | } | ||
207 | }} | ||
208 | /> | ||
209 | </Box> | ||
210 | )} | ||
211 | </SlideInPanel> | ||
212 | ); | ||
213 | } | ||
214 | |||
215 | export default observer(ExportPanel); | ||