mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2025-01-08 16:10:07 -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,17 +1,33 @@
|
||||||
|
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 { registerChange } from './registerChange';
|
||||||
import { isGoogleFont } from './translators/text/font/gfonts';
|
|
||||||
import { isLocalFont } from './translators/text/font/local';
|
|
||||||
|
|
||||||
export const findAllTextNodes = async () => {
|
export const findAllTextNodes = async () => {
|
||||||
await figma.loadAllPagesAsync();
|
|
||||||
|
|
||||||
const nodes = figma.root.findAllWithCriteria({
|
|
||||||
types: ['TEXT']
|
|
||||||
});
|
|
||||||
|
|
||||||
const fonts = new Set<string>();
|
const fonts = new Set<string>();
|
||||||
|
|
||||||
nodes.forEach(node => {
|
for (const page of figma.root.children) {
|
||||||
|
await page.loadAsync();
|
||||||
|
|
||||||
|
for (const node of page.children) {
|
||||||
|
if (node.type === 'TEXT') {
|
||||||
|
extractMissingFonts(node, fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'CUSTOM_FONTS',
|
||||||
|
data: Array.from(fonts)
|
||||||
|
});
|
||||||
|
|
||||||
|
figma.currentPage.once('nodechange', registerChange);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractMissingFonts = (node: TextNode, fonts: Set<string>) => {
|
||||||
const styledTextSegments = node.getStyledTextSegments(['fontName']);
|
const styledTextSegments = node.getStyledTextSegments(['fontName']);
|
||||||
|
|
||||||
styledTextSegments.forEach(segment => {
|
styledTextSegments.forEach(segment => {
|
||||||
|
@ -21,12 +37,4 @@ export const findAllTextNodes = async () => {
|
||||||
|
|
||||||
fonts.add(segment.fontName.family);
|
fonts.add(segment.fontName.family);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
figma.ui.postMessage({
|
|
||||||
type: 'CUSTOM_FONTS',
|
|
||||||
data: Array.from(fonts)
|
|
||||||
});
|
|
||||||
|
|
||||||
figma.currentPage.once('nodechange', registerChange);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { transformDocumentNode } from '@plugin/transformers';
|
||||||
import { setCustomFontId } from '@plugin/translators/text/font/custom';
|
import { setCustomFontId } from '@plugin/translators/text/font/custom';
|
||||||
|
|
||||||
export const handleExportMessage = async (missingFontIds: Record<string, string>) => {
|
export const handleExportMessage = async (missingFontIds: Record<string, string>) => {
|
||||||
await figma.loadAllPagesAsync();
|
|
||||||
|
|
||||||
Object.entries(missingFontIds).forEach(([fontFamily, fontId]) => {
|
Object.entries(missingFontIds).forEach(([fontFamily, fontId]) => {
|
||||||
setCustomFontId(fontFamily, fontId);
|
setCustomFontId(fontFamily, fontId);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,9 +5,28 @@ import { PenpotDocument } from '@ui/types';
|
||||||
import { transformPageNode } from '.';
|
import { transformPageNode } from '.';
|
||||||
|
|
||||||
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => {
|
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 {
|
return {
|
||||||
name: node.name,
|
name: node.name,
|
||||||
children: await Promise.all(node.children.map(child => transformPageNode(child))),
|
children,
|
||||||
components: componentsLibrary.all()
|
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 { translatePageFill } from '@plugin/translators/fills';
|
||||||
|
|
||||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||||
|
@ -9,6 +9,6 @@ export const transformPageNode = async (node: PageNode): Promise<PenpotPage> =>
|
||||||
options: {
|
options: {
|
||||||
background: node.backgrounds.length ? translatePageFill(node.backgrounds[0]) : undefined
|
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,
|
baseX: number = 0,
|
||||||
baseY: number = 0
|
baseY: number = 0
|
||||||
): Promise<PenpotNode | undefined> => {
|
): Promise<PenpotNode | undefined> => {
|
||||||
|
let penpotNode: PenpotNode | undefined;
|
||||||
|
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'PROGRESS_NODE',
|
||||||
|
data: node.name
|
||||||
|
});
|
||||||
|
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'RECTANGLE':
|
case 'RECTANGLE':
|
||||||
return await transformRectangleNode(node, baseX, baseY);
|
penpotNode = await transformRectangleNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'ELLIPSE':
|
case 'ELLIPSE':
|
||||||
return await transformEllipseNode(node, baseX, baseY);
|
penpotNode = await transformEllipseNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'SECTION':
|
case 'SECTION':
|
||||||
case 'FRAME':
|
case 'FRAME':
|
||||||
return await transformFrameNode(node, baseX, baseY);
|
penpotNode = await transformFrameNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'GROUP':
|
case 'GROUP':
|
||||||
return await transformGroupNode(node, baseX, baseY);
|
penpotNode = await transformGroupNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'TEXT':
|
case 'TEXT':
|
||||||
return await transformTextNode(node, baseX, baseY);
|
penpotNode = await transformTextNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'VECTOR':
|
case 'VECTOR':
|
||||||
return await transformVectorNode(node, baseX, baseY);
|
penpotNode = await transformVectorNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'STAR':
|
case 'STAR':
|
||||||
case 'POLYGON':
|
case 'POLYGON':
|
||||||
case 'LINE':
|
case 'LINE':
|
||||||
return await transformPathNode(node, baseX, baseY);
|
penpotNode = await transformPathNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'BOOLEAN_OPERATION':
|
case 'BOOLEAN_OPERATION':
|
||||||
return await transformBooleanNode(node, baseX, baseY);
|
penpotNode = await transformBooleanNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'COMPONENT':
|
case 'COMPONENT':
|
||||||
return await transformComponentNode(node, baseX, baseY);
|
penpotNode = await transformComponentNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
case 'INSTANCE':
|
case 'INSTANCE':
|
||||||
return await transformInstanceNode(node, baseX, baseY);
|
penpotNode = await transformInstanceNode(node, baseX, baseY);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (penpotNode === undefined) {
|
||||||
console.error(`Unsupported node type: ${node.type}`);
|
console.error(`Unsupported node type: ${node.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return penpotNode;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { transformGroupNodeLike, transformSceneNode } from '@plugin/transformers';
|
import { transformGroupNodeLike, transformSceneNode } from '@plugin/transformers';
|
||||||
|
import { sleep } from '@plugin/utils';
|
||||||
|
|
||||||
import { PenpotNode } from '@ui/types';
|
import { PenpotNode } from '@ui/types';
|
||||||
|
|
||||||
|
@ -32,10 +33,18 @@ export const translateMaskChildren = async (
|
||||||
|
|
||||||
export const translateChildren = async (
|
export const translateChildren = async (
|
||||||
children: readonly SceneNode[],
|
children: readonly SceneNode[],
|
||||||
baseX: number,
|
baseX: number = 0,
|
||||||
baseY: number
|
baseY: number = 0
|
||||||
): Promise<PenpotNode[]> => {
|
): Promise<PenpotNode[]> => {
|
||||||
return (await Promise.all(children.map(child => transformSceneNode(child, baseX, baseY)))).filter(
|
const transformedChildren: PenpotNode[] = [];
|
||||||
(child): child is PenpotNode => !!child
|
|
||||||
);
|
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 './getBoundingBox';
|
||||||
export * from './matrixInvert';
|
export * from './matrixInvert';
|
||||||
export * from './rgbToHex';
|
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 (
|
return (
|
||||||
<Wrapper ref={ref} overflowing={(height ?? 0) > MAX_HEIGHT}>
|
<Wrapper ref={ref} overflowing={(height ?? 0) > MAX_HEIGHT}>
|
||||||
<Stack space="medium">
|
<Stack>
|
||||||
<Penpot
|
<Penpot
|
||||||
style={{
|
style={{
|
||||||
alignSelf: 'center',
|
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 { parse } from '@ui/parser';
|
||||||
import { PenpotDocument } from '@ui/types';
|
import { PenpotDocument } from '@ui/types';
|
||||||
|
|
||||||
|
import { ExporterProgress } from './ExporterProgress';
|
||||||
import { MissingFontsSection } from './MissingFontsSection';
|
import { MissingFontsSection } from './MissingFontsSection';
|
||||||
|
|
||||||
type FormValues = Record<string, string>;
|
type FormValues = Record<string, string>;
|
||||||
|
@ -95,16 +96,20 @@ export const PenpotExporter = () => {
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(exportPenpot)}>
|
<form onSubmit={methods.handleSubmit(exportPenpot)}>
|
||||||
<Stack space="medium">
|
<Stack>
|
||||||
<MissingFontsSection fonts={missingFonts} />
|
<MissingFontsSection fonts={missingFonts} />
|
||||||
|
{exporting ? (
|
||||||
|
<ExporterProgress />
|
||||||
|
) : (
|
||||||
<Stack space="xsmall" direction="row">
|
<Stack space="xsmall" direction="row">
|
||||||
<Button type="submit" loading={exporting} fullWidth>
|
<Button type="submit" fullWidth>
|
||||||
Export to Penpot
|
Export to Penpot
|
||||||
</Button>
|
</Button>
|
||||||
<Button secondary onClick={cancel} fullWidth>
|
<Button secondary onClick={cancel} fullWidth>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
Loading…
Reference in a new issue