0
Fork 0
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:
Jordi Sala Morales 2024-05-31 11:25:32 +02:00 committed by GitHub
parent 7b3192936e
commit 88b3e5f69c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 172 additions and 47 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Add progress bar during the export

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Improve performance so the interface of figma feels more responsive during the export process

View file

@ -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);
});
};

View file

@ -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);
});

View file

@ -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()
};
};

View file

@ -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)
};
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -6,3 +6,4 @@ export * from './detectMimeType';
export * from './getBoundingBox';
export * from './matrixInvert';
export * from './rgbToHex';
export * from './sleep';

View file

@ -0,0 +1 @@
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

View file

@ -22,7 +22,7 @@ export const App = () => {
return (
<Wrapper ref={ref} overflowing={(height ?? 0) > MAX_HEIGHT}>
<Stack space="medium">
<Stack>
<Penpot
style={{
alignSelf: 'center',

View 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>
);
};

View file

@ -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>