mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
Implement progress bar (#126)
* Implement progress bar * remove postmessage
This commit is contained in:
parent
7b3192936e
commit
88b3e5f69c
13 changed files with 172 additions and 47 deletions
5
.changeset/sharp-years-hear.md
Normal file
5
.changeset/sharp-years-hear.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": minor
|
||||
---
|
||||
|
||||
Add progress bar during the export
|
5
.changeset/tame-buttons-develop.md
Normal file
5
.changeset/tame-buttons-develop.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": minor
|
||||
---
|
||||
|
||||
Improve performance so the interface of figma feels more responsive during the export process
|
|
@ -1,27 +1,23 @@
|
|||
import { isGoogleFont } from '@plugin/translators/text/font/gfonts';
|
||||
import { isLocalFont } from '@plugin/translators/text/font/local';
|
||||
import { sleep } from '@plugin/utils';
|
||||
|
||||
import { registerChange } from './registerChange';
|
||||
import { isGoogleFont } from './translators/text/font/gfonts';
|
||||
import { isLocalFont } from './translators/text/font/local';
|
||||
|
||||
export const findAllTextNodes = async () => {
|
||||
await figma.loadAllPagesAsync();
|
||||
|
||||
const nodes = figma.root.findAllWithCriteria({
|
||||
types: ['TEXT']
|
||||
});
|
||||
|
||||
const fonts = new Set<string>();
|
||||
|
||||
nodes.forEach(node => {
|
||||
const styledTextSegments = node.getStyledTextSegments(['fontName']);
|
||||
for (const page of figma.root.children) {
|
||||
await page.loadAsync();
|
||||
|
||||
styledTextSegments.forEach(segment => {
|
||||
if (isGoogleFont(segment.fontName) || isLocalFont(segment.fontName)) {
|
||||
return;
|
||||
for (const node of page.children) {
|
||||
if (node.type === 'TEXT') {
|
||||
extractMissingFonts(node, fonts);
|
||||
}
|
||||
|
||||
fonts.add(segment.fontName.family);
|
||||
});
|
||||
});
|
||||
sleep(0);
|
||||
}
|
||||
}
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'CUSTOM_FONTS',
|
||||
|
@ -30,3 +26,15 @@ export const findAllTextNodes = async () => {
|
|||
|
||||
figma.currentPage.once('nodechange', registerChange);
|
||||
};
|
||||
|
||||
const extractMissingFonts = (node: TextNode, fonts: Set<string>) => {
|
||||
const styledTextSegments = node.getStyledTextSegments(['fontName']);
|
||||
|
||||
styledTextSegments.forEach(segment => {
|
||||
if (isGoogleFont(segment.fontName) || isLocalFont(segment.fontName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fonts.add(segment.fontName.family);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -2,8 +2,6 @@ import { transformDocumentNode } from '@plugin/transformers';
|
|||
import { setCustomFontId } from '@plugin/translators/text/font/custom';
|
||||
|
||||
export const handleExportMessage = async (missingFontIds: Record<string, string>) => {
|
||||
await figma.loadAllPagesAsync();
|
||||
|
||||
Object.entries(missingFontIds).forEach(([fontFamily, fontId]) => {
|
||||
setCustomFontId(fontFamily, fontId);
|
||||
});
|
||||
|
|
|
@ -5,9 +5,28 @@ import { PenpotDocument } from '@ui/types';
|
|||
import { transformPageNode } from '.';
|
||||
|
||||
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => {
|
||||
const children = [];
|
||||
let currentPage = 0;
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_TOTAL_PAGES',
|
||||
data: node.children.length
|
||||
});
|
||||
|
||||
for (const page of node.children) {
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_PROCESSED_PAGES',
|
||||
data: currentPage++
|
||||
});
|
||||
|
||||
await page.loadAsync();
|
||||
|
||||
children.push(await transformPageNode(page));
|
||||
}
|
||||
|
||||
return {
|
||||
name: node.name,
|
||||
children: await Promise.all(node.children.map(child => transformPageNode(child))),
|
||||
children,
|
||||
components: componentsLibrary.all()
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { transformChildren } from '@plugin/transformers/partials';
|
||||
import { translateChildren } from '@plugin/translators';
|
||||
import { translatePageFill } from '@plugin/translators/fills';
|
||||
|
||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||
|
@ -9,6 +9,6 @@ export const transformPageNode = async (node: PageNode): Promise<PenpotPage> =>
|
|||
options: {
|
||||
background: node.backgrounds.length ? translatePageFill(node.backgrounds[0]) : undefined
|
||||
},
|
||||
...(await transformChildren(node))
|
||||
children: await translateChildren(node.children)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -18,31 +18,52 @@ export const transformSceneNode = async (
|
|||
baseX: number = 0,
|
||||
baseY: number = 0
|
||||
): Promise<PenpotNode | undefined> => {
|
||||
let penpotNode: PenpotNode | undefined;
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_NODE',
|
||||
data: node.name
|
||||
});
|
||||
|
||||
switch (node.type) {
|
||||
case 'RECTANGLE':
|
||||
return await transformRectangleNode(node, baseX, baseY);
|
||||
penpotNode = await transformRectangleNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'ELLIPSE':
|
||||
return await transformEllipseNode(node, baseX, baseY);
|
||||
penpotNode = await transformEllipseNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'SECTION':
|
||||
case 'FRAME':
|
||||
return await transformFrameNode(node, baseX, baseY);
|
||||
penpotNode = await transformFrameNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'GROUP':
|
||||
return await transformGroupNode(node, baseX, baseY);
|
||||
penpotNode = await transformGroupNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'TEXT':
|
||||
return await transformTextNode(node, baseX, baseY);
|
||||
penpotNode = await transformTextNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'VECTOR':
|
||||
return await transformVectorNode(node, baseX, baseY);
|
||||
penpotNode = await transformVectorNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'STAR':
|
||||
case 'POLYGON':
|
||||
case 'LINE':
|
||||
return await transformPathNode(node, baseX, baseY);
|
||||
penpotNode = await transformPathNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'BOOLEAN_OPERATION':
|
||||
return await transformBooleanNode(node, baseX, baseY);
|
||||
penpotNode = await transformBooleanNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'COMPONENT':
|
||||
return await transformComponentNode(node, baseX, baseY);
|
||||
penpotNode = await transformComponentNode(node, baseX, baseY);
|
||||
break;
|
||||
case 'INSTANCE':
|
||||
return await transformInstanceNode(node, baseX, baseY);
|
||||
penpotNode = await transformInstanceNode(node, baseX, baseY);
|
||||
break;
|
||||
}
|
||||
|
||||
console.error(`Unsupported node type: ${node.type}`);
|
||||
if (penpotNode === undefined) {
|
||||
console.error(`Unsupported node type: ${node.type}`);
|
||||
}
|
||||
|
||||
return penpotNode;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { transformGroupNodeLike, transformSceneNode } from '@plugin/transformers';
|
||||
import { sleep } from '@plugin/utils';
|
||||
|
||||
import { PenpotNode } from '@ui/types';
|
||||
|
||||
|
@ -32,10 +33,18 @@ export const translateMaskChildren = async (
|
|||
|
||||
export const translateChildren = async (
|
||||
children: readonly SceneNode[],
|
||||
baseX: number,
|
||||
baseY: number
|
||||
baseX: number = 0,
|
||||
baseY: number = 0
|
||||
): Promise<PenpotNode[]> => {
|
||||
return (await Promise.all(children.map(child => transformSceneNode(child, baseX, baseY)))).filter(
|
||||
(child): child is PenpotNode => !!child
|
||||
);
|
||||
const transformedChildren: PenpotNode[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
const penpotNode = await transformSceneNode(child, baseX, baseY);
|
||||
|
||||
if (penpotNode) transformedChildren.push(penpotNode);
|
||||
|
||||
await sleep(0);
|
||||
}
|
||||
|
||||
return transformedChildren;
|
||||
};
|
||||
|
|
|
@ -6,3 +6,4 @@ export * from './detectMimeType';
|
|||
export * from './getBoundingBox';
|
||||
export * from './matrixInvert';
|
||||
export * from './rgbToHex';
|
||||
export * from './sleep';
|
||||
|
|
1
plugin-src/utils/sleep.ts
Normal file
1
plugin-src/utils/sleep.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
@ -22,7 +22,7 @@ export const App = () => {
|
|||
|
||||
return (
|
||||
<Wrapper ref={ref} overflowing={(height ?? 0) > MAX_HEIGHT}>
|
||||
<Stack space="medium">
|
||||
<Stack>
|
||||
<Penpot
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
|
|
53
ui-src/components/ExporterProgress.tsx
Normal file
53
ui-src/components/ExporterProgress.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { LoadingIndicator } from '@create-figma-plugin/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Stack } from './Stack';
|
||||
|
||||
export const ExporterProgress = () => {
|
||||
const [currentNode, setCurrentNode] = useState<string | undefined>();
|
||||
const [totalPages, setTotalPages] = useState<number | undefined>();
|
||||
const [processedPages, setProcessedPages] = useState<number | undefined>();
|
||||
|
||||
const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: unknown } }>) => {
|
||||
if (event.data.pluginMessage?.type === 'PROGRESS_NODE') {
|
||||
setCurrentNode(event.data.pluginMessage.data as string);
|
||||
} else if (event.data.pluginMessage?.type === 'PROGRESS_TOTAL_PAGES') {
|
||||
setTotalPages(event.data.pluginMessage.data as number);
|
||||
} else if (event.data.pluginMessage?.type === 'PROGRESS_PROCESSED_PAGES') {
|
||||
setProcessedPages(event.data.pluginMessage.data as number);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', onMessage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const truncateText = (text: string, maxChars: number) => {
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0, maxChars) + '...';
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack space="small" horizontalAlign="center">
|
||||
<LoadingIndicator />
|
||||
<span style={{ textAlign: 'center' }}>
|
||||
{processedPages} of {totalPages} pages exported 💪
|
||||
{currentNode ? (
|
||||
<>
|
||||
<br />
|
||||
Currently exporting layer
|
||||
<br />
|
||||
{'“' + truncateText(currentNode, 40) + '”'}
|
||||
</>
|
||||
) : undefined}
|
||||
</span>
|
||||
</Stack>
|
||||
);
|
||||
};
|
|
@ -6,6 +6,7 @@ import { Stack } from '@ui/components/Stack';
|
|||
import { parse } from '@ui/parser';
|
||||
import { PenpotDocument } from '@ui/types';
|
||||
|
||||
import { ExporterProgress } from './ExporterProgress';
|
||||
import { MissingFontsSection } from './MissingFontsSection';
|
||||
|
||||
type FormValues = Record<string, string>;
|
||||
|
@ -95,16 +96,20 @@ export const PenpotExporter = () => {
|
|||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(exportPenpot)}>
|
||||
<Stack space="medium">
|
||||
<Stack>
|
||||
<MissingFontsSection fonts={missingFonts} />
|
||||
<Stack space="xsmall" direction="row">
|
||||
<Button type="submit" loading={exporting} fullWidth>
|
||||
Export to Penpot
|
||||
</Button>
|
||||
<Button secondary onClick={cancel} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</Stack>
|
||||
{exporting ? (
|
||||
<ExporterProgress />
|
||||
) : (
|
||||
<Stack space="xsmall" direction="row">
|
||||
<Button type="submit" fullWidth>
|
||||
Export to Penpot
|
||||
</Button>
|
||||
<Button secondary onClick={cancel} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
|
Loading…
Reference in a new issue