0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-22 13:43:03 -05:00

Add more loaders so we can track the progress better (#145)

* Add more loaders so we can track the progress better

* fix lint
This commit is contained in:
Jordi Sala Morales 2024-06-06 17:24:59 +02:00 committed by GitHub
parent 78688988d6
commit 1f8f4bf539
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 271 additions and 103 deletions

View file

@ -25,6 +25,10 @@ class RemoteComponentsLibrary {
public remaining(): number { public remaining(): number {
return this.queue.length; return this.queue.length;
} }
public total(): number {
return Object.keys(this.components).length;
}
} }
export const remoteComponentLibrary = new RemoteComponentsLibrary(); export const remoteComponentLibrary = new RemoteComponentsLibrary();

View file

@ -4,16 +4,52 @@ import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
import { translateRemoteChildren } from '@plugin/translators'; import { translateRemoteChildren } from '@plugin/translators';
import { sleep } from '@plugin/utils'; import { sleep } from '@plugin/utils';
import { PenpotPage } from '@ui/lib/types/penpotPage';
import { PenpotDocument } from '@ui/types'; import { PenpotDocument } from '@ui/types';
import { transformPageNode } from '.'; import { transformPageNode } from '.';
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => { const downloadImages = async (): Promise<Record<string, Uint8Array>> => {
const imageToDownload = Object.entries(imagesLibrary.all());
const images: Record<string, Uint8Array> = {};
let currentImage = 1;
figma.ui.postMessage({
type: 'PROGRESS_STEP',
data: 'images'
});
figma.ui.postMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: imageToDownload.length
});
for (const [key, image] of imageToDownload) {
const bytes = await image?.getBytesAsync();
if (bytes) {
images[key] = bytes;
}
figma.ui.postMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: currentImage++
});
await sleep(0);
}
await sleep(20);
return images;
};
const processPages = async (node: DocumentNode): Promise<PenpotPage[]> => {
const children = []; const children = [];
let currentPage = 1; let currentPage = 1;
figma.ui.postMessage({ figma.ui.postMessage({
type: 'PROGRESS_TOTAL_PAGES', type: 'PROGRESS_TOTAL_ITEMS',
data: node.children.length data: node.children.length
}); });
@ -23,13 +59,19 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
children.push(await transformPageNode(page)); children.push(await transformPageNode(page));
figma.ui.postMessage({ figma.ui.postMessage({
type: 'PROGRESS_PROCESSED_PAGES', type: 'PROGRESS_PROCESSED_ITEMS',
data: currentPage++ data: currentPage++
}); });
await sleep(0); await sleep(0);
} }
return children;
};
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => {
const children = await processPages(node);
if (remoteComponentLibrary.remaining() > 0) { if (remoteComponentLibrary.remaining() > 0) {
children.push({ children.push({
name: 'External Components', name: 'External Components',
@ -37,20 +79,10 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
}); });
} }
const images: Record<string, Uint8Array> = {};
for (const [key, image] of Object.entries(imagesLibrary.all())) {
const bytes = await image?.getBytesAsync();
if (!bytes) continue;
images[key] = bytes;
}
return { return {
name: node.name, name: node.name,
children, children,
components: componentsLibrary.all(), components: componentsLibrary.all(),
images images: await downloadImages()
}; };
}; };

View file

@ -21,7 +21,7 @@ export const transformSceneNode = async (
let penpotNode: PenpotNode | undefined; let penpotNode: PenpotNode | undefined;
figma.ui.postMessage({ figma.ui.postMessage({
type: 'PROGRESS_NODE', type: 'PROGRESS_CURRENT_ITEM',
data: node.name data: node.name
}); });

View file

@ -54,14 +54,30 @@ export const translateChildren = async (
export const translateRemoteChildren = async (): Promise<PenpotNode[]> => { export const translateRemoteChildren = async (): Promise<PenpotNode[]> => {
const transformedChildren: PenpotNode[] = []; const transformedChildren: PenpotNode[] = [];
let currentRemote = 1;
figma.ui.postMessage({
type: 'PROGRESS_STEP',
data: 'remote'
});
while (remoteComponentLibrary.remaining() > 0) { while (remoteComponentLibrary.remaining() > 0) {
figma.ui.postMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: remoteComponentLibrary.total()
});
const child = remoteComponentLibrary.next(); const child = remoteComponentLibrary.next();
const penpotNode = await transformSceneNode(child); const penpotNode = await transformSceneNode(child);
if (penpotNode) transformedChildren.push(penpotNode); if (penpotNode) transformedChildren.push(penpotNode);
figma.ui.postMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: currentRemote++
});
await sleep(0); await sleep(0);
} }

View file

@ -1,11 +1,38 @@
import { LoadingIndicator } from '@create-figma-plugin/ui'; import { LoadingIndicator } from '@create-figma-plugin/ui';
import { JSX } from 'react';
import { useFigmaContext } from '@ui/context'; import { Steps, useFigmaContext } from '@ui/context';
import { Stack } from './Stack'; import { Stack } from './Stack';
export const ExporterProgress = () => { type Messages = {
const { currentNode, totalPages, processedPages, downloading } = useFigmaContext(); total: string;
current?: string;
};
const stepMessages: Record<Steps, Messages> = {
processing: {
total: 'pages processed 💪',
current: 'Currently processing layer'
},
remote: {
total: 'remote components processed 📦',
current: 'Currently processing layer'
},
images: {
total: 'images downloaded 📸'
},
optimization: {
total: 'images optimized 📸'
},
downloading: {
total: 'Generating Penpot file 🚀',
current: 'Please wait, this process might take a while...'
}
};
const StepProgress = (): JSX.Element | null => {
const { currentItem, totalItems, processedItems, step } = useFigmaContext();
const truncateText = (text: string, maxChars: number) => { const truncateText = (text: string, maxChars: number) => {
if (text.length <= maxChars) { if (text.length <= maxChars) {
@ -15,29 +42,45 @@ export const ExporterProgress = () => {
return text.slice(0, maxChars) + '...'; return text.slice(0, maxChars) + '...';
}; };
if (!step) return null;
const currentText = stepMessages[step].current;
switch (step) {
case 'processing':
case 'remote':
case 'images':
case 'optimization':
return (
<>
{processedItems} of {totalItems} {stepMessages[step].total}
{currentItem && currentText ? (
<>
<br />
{currentText}
<br />
{'“' + truncateText(currentItem, 35) + '”'}
</>
) : undefined}
</>
);
case 'downloading':
return (
<>
{stepMessages[step].total}
<br />
{currentText}
</>
);
}
};
export const ExporterProgress = () => {
return ( return (
<Stack space="small" horizontalAlign="center"> <Stack space="small" horizontalAlign="center">
<LoadingIndicator /> <LoadingIndicator />
<span style={{ textAlign: 'center' }}> <span style={{ textAlign: 'center' }}>
{!downloading ? ( <StepProgress />
<>
{processedPages} of {totalPages} pages exported 💪
{currentNode ? (
<>
<br />
Currently exporting layer
<br />
{'“' + truncateText(currentNode, 35) + '”'}
</>
) : undefined}
</>
) : (
<>
Generating Penpot file 🚀
<br />
Please wait, this process might take a while...
</>
)}
</span> </span>
</Stack> </Stack>
); );

View file

@ -1,2 +1,4 @@
export * from './createGenericContext'; export * from './createGenericContext';
export * from './FigmaContext'; export * from './FigmaContext';
export * from './messages';
export * from './useFigma';

View file

@ -0,0 +1,58 @@
import { PenpotDocument } from '@ui/types';
import { Steps } from '.';
export type MessageData = { pluginMessage?: PluginMessage };
type PluginMessage =
| PenpotDocumentMessage
| CustomFontsMessage
| ChangesDetectedMessage
| ProgressStepMessage
| ProgressCurrentItemMessage
| ProgressTotalItemsMessage
| ProgressProcessedItemsMessage;
type PenpotDocumentMessage = {
type: 'PENPOT_DOCUMENT';
data: PenpotDocument;
};
type CustomFontsMessage = {
type: 'CUSTOM_FONTS';
data: string[];
};
type ChangesDetectedMessage = {
type: 'CHANGES_DETECTED';
};
type ProgressStepMessage = {
type: 'PROGRESS_STEP';
data: Steps;
};
type ProgressCurrentItemMessage = {
type: 'PROGRESS_CURRENT_ITEM';
data: string;
};
type ProgressTotalItemsMessage = {
type: 'PROGRESS_TOTAL_ITEMS';
data: number;
};
type ProgressProcessedItemsMessage = {
type: 'PROGRESS_PROCESSED_ITEMS';
data: number;
};
export const sendMessage = (pluginMessage: PluginMessage) => {
window.dispatchEvent(
new MessageEvent<MessageData>('message', {
data: {
pluginMessage
}
})
);
};

View file

@ -2,89 +2,54 @@ import { useEffect, useState } from 'react';
import { FormValues } from '@ui/components/ExportForm'; import { FormValues } from '@ui/components/ExportForm';
import { parse } from '@ui/parser'; import { parse } from '@ui/parser';
import { PenpotDocument } from '@ui/types';
import { MessageData } from '.';
export type UseFigmaHook = { export type UseFigmaHook = {
missingFonts: string[] | undefined; missingFonts: string[] | undefined;
needsReload: boolean; needsReload: boolean;
loading: boolean; loading: boolean;
exporting: boolean; exporting: boolean;
downloading: boolean; step: Steps | undefined;
currentNode: string | undefined; currentItem: string | undefined;
totalPages: number | undefined; totalItems: number;
processedPages: number | undefined; processedItems: number;
reload: () => void; reload: () => void;
cancel: () => void; cancel: () => void;
exportPenpot: (data: FormValues) => void; exportPenpot: (data: FormValues) => void;
}; };
type PluginMessage = export type Steps = 'processing' | 'remote' | 'images' | 'optimization' | 'downloading';
| PenpotDocumentMessage
| CustomFontsMessage
| ChangesDetectedMessage
| ProgressNodeMessage
| ProgressTotalPagesMessage
| ProgressProcessedPagesMessage;
type PenpotDocumentMessage = {
type: 'PENPOT_DOCUMENT';
data: PenpotDocument;
};
type CustomFontsMessage = {
type: 'CUSTOM_FONTS';
data: string[];
};
type ChangesDetectedMessage = {
type: 'CHANGES_DETECTED';
};
type ProgressNodeMessage = {
type: 'PROGRESS_NODE';
data: string;
};
type ProgressTotalPagesMessage = {
type: 'PROGRESS_TOTAL_PAGES';
data: number;
};
type ProgressProcessedPagesMessage = {
type: 'PROGRESS_PROCESSED_PAGES';
data: number;
};
export const useFigma = (): UseFigmaHook => { export const useFigma = (): UseFigmaHook => {
const [missingFonts, setMissingFonts] = useState<string[]>(); const [missingFonts, setMissingFonts] = useState<string[]>();
const [needsReload, setNeedsReload] = useState(false); const [needsReload, setNeedsReload] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [exporting, setExporting] = useState(false); const [exporting, setExporting] = useState(false);
const [downloading, setDownloading] = useState(false);
const [currentNode, setCurrentNode] = useState<string | undefined>(); const [step, setStep] = useState<Steps>();
const [totalPages, setTotalPages] = useState<number | undefined>(); const [currentItem, setCurrentItem] = useState<string | undefined>();
const [processedPages, setProcessedPages] = useState<number | undefined>(); const [totalItems, setTotalItems] = useState<number>(0);
const [processedItems, setProcessedItems] = useState<number>(0);
const postMessage = (type: string, data?: unknown) => { const postMessage = (type: string, data?: unknown) => {
parent.postMessage({ pluginMessage: { type, data } }, '*'); parent.postMessage({ pluginMessage: { type, data } }, '*');
}; };
const onMessage = async (event: MessageEvent<{ pluginMessage?: PluginMessage }>) => { const onMessage = async (event: MessageEvent<MessageData>) => {
if (!event.data.pluginMessage) return; if (!event.data.pluginMessage) return;
const { pluginMessage } = event.data; const { pluginMessage } = event.data;
switch (pluginMessage.type) { switch (pluginMessage.type) {
case 'PENPOT_DOCUMENT': { case 'PENPOT_DOCUMENT': {
setDownloading(true);
const file = await parse(pluginMessage.data); const file = await parse(pluginMessage.data);
const blob = await file.export(); const blob = await file.export();
download(blob, `${pluginMessage.data.name}.zip`); download(blob, `${pluginMessage.data.name}.zip`);
setExporting(false); setExporting(false);
setDownloading(false); setStep(undefined);
break; break;
} }
@ -98,17 +63,21 @@ export const useFigma = (): UseFigmaHook => {
setNeedsReload(true); setNeedsReload(true);
break; break;
} }
case 'PROGRESS_NODE': { case 'PROGRESS_STEP': {
setCurrentNode(pluginMessage.data); setStep(pluginMessage.data);
setProcessedItems(0);
break; break;
} }
case 'PROGRESS_TOTAL_PAGES': { case 'PROGRESS_CURRENT_ITEM': {
setTotalPages(pluginMessage.data); setCurrentItem(pluginMessage.data);
setProcessedPages(0);
break; break;
} }
case 'PROGRESS_PROCESSED_PAGES': { case 'PROGRESS_TOTAL_ITEMS': {
setProcessedPages(pluginMessage.data); setTotalItems(pluginMessage.data);
break;
}
case 'PROGRESS_PROCESSED_ITEMS': {
setProcessedItems(pluginMessage.data);
break; break;
} }
} }
@ -135,6 +104,8 @@ export const useFigma = (): UseFigmaHook => {
const exportPenpot = (data: FormValues) => { const exportPenpot = (data: FormValues) => {
setExporting(true); setExporting(true);
setStep('processing');
setProcessedItems(0);
postMessage('export', data); postMessage('export', data);
}; };
@ -154,10 +125,10 @@ export const useFigma = (): UseFigmaHook => {
needsReload, needsReload,
loading, loading,
exporting, exporting,
downloading, step,
currentNode, currentItem,
totalPages, totalItems,
processedPages, processedItems,
reload, reload,
cancel, cancel,
exportPenpot exportPenpot

View file

@ -1,12 +1,20 @@
// @TODO: Direct import on purpose, to avoid problems with the tsc linting
import { sleep } from '@plugin/utils/sleep';
import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { PenpotPage } from '@ui/lib/types/penpotPage'; import { PenpotPage } from '@ui/lib/types/penpotPage';
import { createItems } from '.'; import { createItems } from '.';
export const createPage = (file: PenpotFile, { name, options, children = [] }: PenpotPage) => { export const createPage = async (
file: PenpotFile,
{ name, options, children = [] }: PenpotPage
) => {
file.addPage(name, options); file.addPage(name, options);
createItems(file, children); createItems(file, children);
await sleep(0);
file.closePage(); file.closePage();
}; };

View file

@ -1,5 +1,8 @@
import { componentsLibrary } from '@plugin/ComponentLibrary'; import { componentsLibrary } from '@plugin/ComponentLibrary';
// @TODO: Direct import on purpose, to avoid problems with the tsc linting
import { sleep } from '@plugin/utils/sleep';
import { sendMessage } from '@ui/context';
import { createFile } from '@ui/lib/penpot'; import { createFile } from '@ui/lib/penpot';
import { createComponentLibrary, createPage } from '@ui/parser/creators'; import { createComponentLibrary, createPage } from '@ui/parser/creators';
import { uiComponents, uiImages } from '@ui/parser/libraries'; import { uiComponents, uiImages } from '@ui/parser/libraries';
@ -7,14 +10,45 @@ import { PenpotDocument } from '@ui/types';
import { idLibrary, parseImage } from '.'; import { idLibrary, parseImage } from '.';
const optimizeImages = async (images: Record<string, Uint8Array>) => {
const imagesToOptimize = Object.entries(images);
let imagesOptimized = 1;
sendMessage({
type: 'PROGRESS_STEP',
data: 'optimization'
});
sendMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: imagesToOptimize.length
});
for (const [key, bytes] of imagesToOptimize) {
if (bytes) {
uiImages.register(key, await parseImage(bytes));
}
sendMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: imagesOptimized++
});
await sleep(0);
}
};
export const parse = async ({ name, children = [], components, images }: PenpotDocument) => { export const parse = async ({ name, children = [], components, images }: PenpotDocument) => {
componentsLibrary.init(components); componentsLibrary.init(components);
for (const [key, bytes] of Object.entries(images)) { await optimizeImages(images);
if (!bytes) continue;
uiImages.register(key, await parseImage(bytes)); sendMessage({
} type: 'PROGRESS_STEP',
data: 'downloading'
});
await sleep(20);
uiComponents.init(); uiComponents.init();
idLibrary.init(); idLibrary.init();
@ -22,7 +56,7 @@ export const parse = async ({ name, children = [], components, images }: PenpotD
const file = createFile(name); const file = createFile(name);
for (const page of children) { for (const page of children) {
createPage(file, page); await createPage(file, page);
} }
createComponentLibrary(file); createComponentLibrary(file);