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:
parent
78688988d6
commit
1f8f4bf539
10 changed files with 271 additions and 103 deletions
|
@ -25,6 +25,10 @@ class RemoteComponentsLibrary {
|
|||
public remaining(): number {
|
||||
return this.queue.length;
|
||||
}
|
||||
|
||||
public total(): number {
|
||||
return Object.keys(this.components).length;
|
||||
}
|
||||
}
|
||||
|
||||
export const remoteComponentLibrary = new RemoteComponentsLibrary();
|
||||
|
|
|
@ -4,16 +4,52 @@ import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
|
|||
import { translateRemoteChildren } from '@plugin/translators';
|
||||
import { sleep } from '@plugin/utils';
|
||||
|
||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||
import { PenpotDocument } from '@ui/types';
|
||||
|
||||
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 = [];
|
||||
let currentPage = 1;
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_TOTAL_PAGES',
|
||||
type: 'PROGRESS_TOTAL_ITEMS',
|
||||
data: node.children.length
|
||||
});
|
||||
|
||||
|
@ -23,13 +59,19 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
|
|||
children.push(await transformPageNode(page));
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_PROCESSED_PAGES',
|
||||
type: 'PROGRESS_PROCESSED_ITEMS',
|
||||
data: currentPage++
|
||||
});
|
||||
|
||||
await sleep(0);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => {
|
||||
const children = await processPages(node);
|
||||
|
||||
if (remoteComponentLibrary.remaining() > 0) {
|
||||
children.push({
|
||||
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 {
|
||||
name: node.name,
|
||||
children,
|
||||
components: componentsLibrary.all(),
|
||||
images
|
||||
images: await downloadImages()
|
||||
};
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ export const transformSceneNode = async (
|
|||
let penpotNode: PenpotNode | undefined;
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_NODE',
|
||||
type: 'PROGRESS_CURRENT_ITEM',
|
||||
data: node.name
|
||||
});
|
||||
|
||||
|
|
|
@ -54,14 +54,30 @@ export const translateChildren = async (
|
|||
|
||||
export const translateRemoteChildren = async (): Promise<PenpotNode[]> => {
|
||||
const transformedChildren: PenpotNode[] = [];
|
||||
let currentRemote = 1;
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_STEP',
|
||||
data: 'remote'
|
||||
});
|
||||
|
||||
while (remoteComponentLibrary.remaining() > 0) {
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_TOTAL_ITEMS',
|
||||
data: remoteComponentLibrary.total()
|
||||
});
|
||||
|
||||
const child = remoteComponentLibrary.next();
|
||||
|
||||
const penpotNode = await transformSceneNode(child);
|
||||
|
||||
if (penpotNode) transformedChildren.push(penpotNode);
|
||||
|
||||
figma.ui.postMessage({
|
||||
type: 'PROGRESS_PROCESSED_ITEMS',
|
||||
data: currentRemote++
|
||||
});
|
||||
|
||||
await sleep(0);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,38 @@
|
|||
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';
|
||||
|
||||
export const ExporterProgress = () => {
|
||||
const { currentNode, totalPages, processedPages, downloading } = useFigmaContext();
|
||||
type Messages = {
|
||||
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) => {
|
||||
if (text.length <= maxChars) {
|
||||
|
@ -15,29 +42,45 @@ export const ExporterProgress = () => {
|
|||
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 (
|
||||
<Stack space="small" horizontalAlign="center">
|
||||
<LoadingIndicator />
|
||||
<span style={{ textAlign: 'center' }}>
|
||||
{!downloading ? (
|
||||
<>
|
||||
{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...
|
||||
</>
|
||||
)}
|
||||
<StepProgress />
|
||||
</span>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
export * from './createGenericContext';
|
||||
export * from './FigmaContext';
|
||||
export * from './messages';
|
||||
export * from './useFigma';
|
||||
|
|
58
ui-src/context/messages.ts
Normal file
58
ui-src/context/messages.ts
Normal 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
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -2,89 +2,54 @@ import { useEffect, useState } from 'react';
|
|||
|
||||
import { FormValues } from '@ui/components/ExportForm';
|
||||
import { parse } from '@ui/parser';
|
||||
import { PenpotDocument } from '@ui/types';
|
||||
|
||||
import { MessageData } from '.';
|
||||
|
||||
export type UseFigmaHook = {
|
||||
missingFonts: string[] | undefined;
|
||||
needsReload: boolean;
|
||||
loading: boolean;
|
||||
exporting: boolean;
|
||||
downloading: boolean;
|
||||
currentNode: string | undefined;
|
||||
totalPages: number | undefined;
|
||||
processedPages: number | undefined;
|
||||
step: Steps | undefined;
|
||||
currentItem: string | undefined;
|
||||
totalItems: number;
|
||||
processedItems: number;
|
||||
reload: () => void;
|
||||
cancel: () => void;
|
||||
exportPenpot: (data: FormValues) => void;
|
||||
};
|
||||
|
||||
type PluginMessage =
|
||||
| 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 type Steps = 'processing' | 'remote' | 'images' | 'optimization' | 'downloading';
|
||||
|
||||
export const useFigma = (): UseFigmaHook => {
|
||||
const [missingFonts, setMissingFonts] = useState<string[]>();
|
||||
const [needsReload, setNeedsReload] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [currentNode, setCurrentNode] = useState<string | undefined>();
|
||||
const [totalPages, setTotalPages] = useState<number | undefined>();
|
||||
const [processedPages, setProcessedPages] = useState<number | undefined>();
|
||||
|
||||
const [step, setStep] = useState<Steps>();
|
||||
const [currentItem, setCurrentItem] = useState<string | undefined>();
|
||||
const [totalItems, setTotalItems] = useState<number>(0);
|
||||
const [processedItems, setProcessedItems] = useState<number>(0);
|
||||
|
||||
const postMessage = (type: string, data?: unknown) => {
|
||||
parent.postMessage({ pluginMessage: { type, data } }, '*');
|
||||
};
|
||||
|
||||
const onMessage = async (event: MessageEvent<{ pluginMessage?: PluginMessage }>) => {
|
||||
const onMessage = async (event: MessageEvent<MessageData>) => {
|
||||
if (!event.data.pluginMessage) return;
|
||||
|
||||
const { pluginMessage } = event.data;
|
||||
|
||||
switch (pluginMessage.type) {
|
||||
case 'PENPOT_DOCUMENT': {
|
||||
setDownloading(true);
|
||||
|
||||
const file = await parse(pluginMessage.data);
|
||||
const blob = await file.export();
|
||||
|
||||
download(blob, `${pluginMessage.data.name}.zip`);
|
||||
|
||||
setExporting(false);
|
||||
setDownloading(false);
|
||||
setStep(undefined);
|
||||
|
||||
break;
|
||||
}
|
||||
|
@ -98,17 +63,21 @@ export const useFigma = (): UseFigmaHook => {
|
|||
setNeedsReload(true);
|
||||
break;
|
||||
}
|
||||
case 'PROGRESS_NODE': {
|
||||
setCurrentNode(pluginMessage.data);
|
||||
case 'PROGRESS_STEP': {
|
||||
setStep(pluginMessage.data);
|
||||
setProcessedItems(0);
|
||||
break;
|
||||
}
|
||||
case 'PROGRESS_TOTAL_PAGES': {
|
||||
setTotalPages(pluginMessage.data);
|
||||
setProcessedPages(0);
|
||||
case 'PROGRESS_CURRENT_ITEM': {
|
||||
setCurrentItem(pluginMessage.data);
|
||||
break;
|
||||
}
|
||||
case 'PROGRESS_PROCESSED_PAGES': {
|
||||
setProcessedPages(pluginMessage.data);
|
||||
case 'PROGRESS_TOTAL_ITEMS': {
|
||||
setTotalItems(pluginMessage.data);
|
||||
break;
|
||||
}
|
||||
case 'PROGRESS_PROCESSED_ITEMS': {
|
||||
setProcessedItems(pluginMessage.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +104,8 @@ export const useFigma = (): UseFigmaHook => {
|
|||
|
||||
const exportPenpot = (data: FormValues) => {
|
||||
setExporting(true);
|
||||
setStep('processing');
|
||||
setProcessedItems(0);
|
||||
|
||||
postMessage('export', data);
|
||||
};
|
||||
|
@ -154,10 +125,10 @@ export const useFigma = (): UseFigmaHook => {
|
|||
needsReload,
|
||||
loading,
|
||||
exporting,
|
||||
downloading,
|
||||
currentNode,
|
||||
totalPages,
|
||||
processedPages,
|
||||
step,
|
||||
currentItem,
|
||||
totalItems,
|
||||
processedItems,
|
||||
reload,
|
||||
cancel,
|
||||
exportPenpot
|
||||
|
|
|
@ -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 { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||
|
||||
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);
|
||||
|
||||
createItems(file, children);
|
||||
|
||||
await sleep(0);
|
||||
|
||||
file.closePage();
|
||||
};
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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 { createComponentLibrary, createPage } from '@ui/parser/creators';
|
||||
import { uiComponents, uiImages } from '@ui/parser/libraries';
|
||||
|
@ -7,14 +10,45 @@ import { PenpotDocument } from '@ui/types';
|
|||
|
||||
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) => {
|
||||
componentsLibrary.init(components);
|
||||
|
||||
for (const [key, bytes] of Object.entries(images)) {
|
||||
if (!bytes) continue;
|
||||
await optimizeImages(images);
|
||||
|
||||
uiImages.register(key, await parseImage(bytes));
|
||||
}
|
||||
sendMessage({
|
||||
type: 'PROGRESS_STEP',
|
||||
data: 'downloading'
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
|
||||
uiComponents.init();
|
||||
idLibrary.init();
|
||||
|
@ -22,7 +56,7 @@ export const parse = async ({ name, children = [], components, images }: PenpotD
|
|||
const file = createFile(name);
|
||||
|
||||
for (const page of children) {
|
||||
createPage(file, page);
|
||||
await createPage(file, page);
|
||||
}
|
||||
|
||||
createComponentLibrary(file);
|
||||
|
|
Loading…
Reference in a new issue