diff --git a/plugin-src/RemoteComponentLibrary.ts b/plugin-src/RemoteComponentLibrary.ts index 69a0ca7..fd53770 100644 --- a/plugin-src/RemoteComponentLibrary.ts +++ b/plugin-src/RemoteComponentLibrary.ts @@ -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(); diff --git a/plugin-src/transformers/transformDocumentNode.ts b/plugin-src/transformers/transformDocumentNode.ts index 8371f2d..236d356 100644 --- a/plugin-src/transformers/transformDocumentNode.ts +++ b/plugin-src/transformers/transformDocumentNode.ts @@ -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 => { +const downloadImages = async (): Promise> => { + const imageToDownload = Object.entries(imagesLibrary.all()); + const images: Record = {}; + 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 => { 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 => { + 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 = {}; - - 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() }; }; diff --git a/plugin-src/transformers/transformSceneNode.ts b/plugin-src/transformers/transformSceneNode.ts index 0cadc88..93af76e 100644 --- a/plugin-src/transformers/transformSceneNode.ts +++ b/plugin-src/transformers/transformSceneNode.ts @@ -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 }); diff --git a/plugin-src/translators/translateChildren.ts b/plugin-src/translators/translateChildren.ts index b4c522c..f11ce55 100644 --- a/plugin-src/translators/translateChildren.ts +++ b/plugin-src/translators/translateChildren.ts @@ -54,14 +54,30 @@ export const translateChildren = async ( export const translateRemoteChildren = async (): Promise => { 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); } diff --git a/ui-src/components/ExporterProgress.tsx b/ui-src/components/ExporterProgress.tsx index 75b3943..5781237 100644 --- a/ui-src/components/ExporterProgress.tsx +++ b/ui-src/components/ExporterProgress.tsx @@ -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 = { + 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 ? ( + <> +
+ {currentText} +
+ {'“' + truncateText(currentItem, 35) + '”'} + + ) : undefined} + + ); + case 'downloading': + return ( + <> + {stepMessages[step].total} +
+ {currentText} + + ); + } +}; + +export const ExporterProgress = () => { return ( - {!downloading ? ( - <> - {processedPages} of {totalPages} pages exported 💪 - {currentNode ? ( - <> -
- Currently exporting layer -
- {'“' + truncateText(currentNode, 35) + '”'} - - ) : undefined} - - ) : ( - <> - Generating Penpot file 🚀 -
- Please wait, this process might take a while... - - )} +
); diff --git a/ui-src/context/index.ts b/ui-src/context/index.ts index 5c18f31..31d6928 100644 --- a/ui-src/context/index.ts +++ b/ui-src/context/index.ts @@ -1,2 +1,4 @@ export * from './createGenericContext'; export * from './FigmaContext'; +export * from './messages'; +export * from './useFigma'; diff --git a/ui-src/context/messages.ts b/ui-src/context/messages.ts new file mode 100644 index 0000000..3009c84 --- /dev/null +++ b/ui-src/context/messages.ts @@ -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('message', { + data: { + pluginMessage + } + }) + ); +}; diff --git a/ui-src/context/useFigma.ts b/ui-src/context/useFigma.ts index 600c197..c71b0a3 100644 --- a/ui-src/context/useFigma.ts +++ b/ui-src/context/useFigma.ts @@ -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(); const [needsReload, setNeedsReload] = useState(false); const [loading, setLoading] = useState(true); const [exporting, setExporting] = useState(false); - const [downloading, setDownloading] = useState(false); - const [currentNode, setCurrentNode] = useState(); - const [totalPages, setTotalPages] = useState(); - const [processedPages, setProcessedPages] = useState(); + + const [step, setStep] = useState(); + const [currentItem, setCurrentItem] = useState(); + const [totalItems, setTotalItems] = useState(0); + const [processedItems, setProcessedItems] = useState(0); const postMessage = (type: string, data?: unknown) => { parent.postMessage({ pluginMessage: { type, data } }, '*'); }; - const onMessage = async (event: MessageEvent<{ pluginMessage?: PluginMessage }>) => { + const onMessage = async (event: MessageEvent) => { 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 diff --git a/ui-src/parser/creators/createPage.ts b/ui-src/parser/creators/createPage.ts index 3e9cb18..2f5b645 100644 --- a/ui-src/parser/creators/createPage.ts +++ b/ui-src/parser/creators/createPage.ts @@ -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(); }; diff --git a/ui-src/parser/parse.ts b/ui-src/parser/parse.ts index d7877ba..e0335ad 100644 --- a/ui-src/parser/parse.ts +++ b/ui-src/parser/parse.ts @@ -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) => { + 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);