diff --git a/apps/remix-ide/src/app/plugins/contractFlattener.tsx b/apps/remix-ide/src/app/plugins/contractFlattener.tsx index 71c3a4a1c1..28d5b33ed1 100644 --- a/apps/remix-ide/src/app/plugins/contractFlattener.tsx +++ b/apps/remix-ide/src/app/plugins/contractFlattener.tsx @@ -3,6 +3,8 @@ import { Plugin } from '@remixproject/engine' import { customAction } from '@remixproject/plugin-api' import { concatSourceFiles, getDependencyGraph } from '@remix-ui/solidity-compiler' +const _paq = window._paq = window._paq || [] + const profile = { name: 'contractflattener', displayName: 'Contract Flattener', @@ -22,6 +24,7 @@ export class ContractFlattener extends Plugin { this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => { await this.flattenContract(source, this.fileName, data) }) + _paq.push(['trackEvent', 'plugin', 'activated', 'contractFlattener']) } async flattenAContract(action: customAction) { @@ -33,7 +36,7 @@ export class ContractFlattener extends Plugin { * Takes currently compiled contract that has a bunch of imports at the top * and flattens them ready for UML creation. Takes the flattened result * and assigns to a local property - * @returns {Promise} + * @returns {Promise} */ async flattenContract (source: any, filePath: string, data: any) { const ast = data.sources @@ -44,6 +47,6 @@ export class ContractFlattener extends Plugin { const sources = source.sources const result = concatSourceFiles(sorted, sources) await this.call('fileManager', 'writeFile', `${filePath}_flattened.sol`, result) - return result + _paq.push(['trackEvent', 'plugin', 'contractFlattener', 'flattenAContract']) } } \ No newline at end of file diff --git a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx index 2c473a86f3..8c9341f6d5 100644 --- a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx +++ b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx @@ -13,6 +13,8 @@ import { PluginViewWrapper } from '@remix-ui/helper' import { customAction } from '@remixproject/plugin-api' const parser = (window as any).SolidityParser +const _paq = window._paq = window._paq || [] + const profile = { name: 'solidityumlgen', displayName: 'Solidity UML Generator', @@ -81,6 +83,7 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen { const umlDot = convertUmlClasses2Dot(umlClasses) const payload = vizRenderStringSync(umlDot) this.updatedSvg = payload + _paq.push(['trackEvent', 'solidityumlgen', 'umlgenerated']) this.renderComponent() await this.call('tabs', 'focus', 'solidityumlgen') } catch (error) { @@ -115,6 +118,7 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen { generateCustomAction = async (action: customAction) => { this.updatedSvg = this.updatedSvg.startsWith(' } diff --git a/apps/remix-ide/src/app/tabs/locales/en/solUmlgen.json b/apps/remix-ide/src/app/tabs/locales/en/solUmlgen.json new file mode 100644 index 0000000000..ca63867e93 --- /dev/null +++ b/apps/remix-ide/src/app/tabs/locales/en/solUmlgen.json @@ -0,0 +1,4 @@ +{ + "solUml.pngDownload": "Download as PNG", + "solUml.pdfDownload": "Download as PDF" +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/locales/zh/solUmlgen.json b/apps/remix-ide/src/app/tabs/locales/zh/solUmlgen.json new file mode 100644 index 0000000000..c5f9d83bc1 --- /dev/null +++ b/apps/remix-ide/src/app/tabs/locales/zh/solUmlgen.json @@ -0,0 +1,4 @@ +{ + "solUml.PngDownload": "sfg", + "solUml.PdfDownload": "sdf" +} \ No newline at end of file diff --git a/libs/remix-ui/solidity-uml-gen/src/lib/components/UmlDownload.tsx b/libs/remix-ui/solidity-uml-gen/src/lib/components/UmlDownload.tsx new file mode 100644 index 0000000000..6872e1a08e --- /dev/null +++ b/libs/remix-ui/solidity-uml-gen/src/lib/components/UmlDownload.tsx @@ -0,0 +1,143 @@ +import { CustomTooltip } from '@remix-ui/helper' +import React, { Fragment, Ref } from 'react' +import { Dropdown } from 'react-bootstrap' +import { UmlFileType } from '../utilities/UmlDownloadStrategy' + +const _paq = (window._paq = window._paq || []) + +export const Markup = React.forwardRef( + ( + { + children, + onClick, + icon, + className = "", + }: { + children: React.ReactNode + onClick: (e) => void + icon: string + className: string + }, + ref: Ref + ) => ( + + ) +) + +export const UmlCustomMenu = React.forwardRef( + ( + { + children, + style, + className, + "aria-labelledby": labeledBy, + }: { + children: React.ReactNode + style?: React.CSSProperties + className: string + "aria-labelledby"?: string + }, + ref: Ref + ) => { + const height = window.innerHeight * 0.6 + return ( +
+
    + {children} +
+
+ ) + } +) + +interface UmlDownloadProps { + download: (fileType: UmlFileType) => void +} + +export default function UmlDownload(props: UmlDownloadProps) { + return ( + + + + + { + _paq.push([ + "trackEvent", + "solidityumlgen", + "download", + "downloadAsPng", + ]); + props.download("png") + }} + > + +
+ + Download as PNG +
+
+
+ + { + _paq.push([ + "trackEvent", + "solUmlgen", + "download", + "downloadAsPdf", + ]); + props.download("pdf") + }} + > + +
+ + Download as PDF +
+
+
+
+
+
+ ) +} diff --git a/libs/remix-ui/solidity-uml-gen/src/lib/css/solidity-uml-gen.css b/libs/remix-ui/solidity-uml-gen/src/lib/css/solidity-uml-gen.css index bc9cf7fd0e..d421d2546c 100644 --- a/libs/remix-ui/solidity-uml-gen/src/lib/css/solidity-uml-gen.css +++ b/libs/remix-ui/solidity-uml-gen/src/lib/css/solidity-uml-gen.css @@ -6,4 +6,32 @@ border-width: 1px; border-style: solid; border-color: var(--info); +} + +#solUmlMenuDropdown > div > ul > a:hover { + background-color: var(--secondary); + border-radius: 2px; + color: var(--text) +} + +.custom-dropdown-items { + padding: 0.25rem 0.25rem; + border-radius: .25rem; + background: var(--custom-select); +} + +.custom-dropdown-items a { + border-radius: .25rem; + text-transform: none; + text-decoration: none; + font-weight: normal; + font-size: 0.875rem; + padding: 0.25rem 0.25rem; + width: auto; + color: var(--text); +} + +.uml-btn-icon { + width: 0.5rem; + height: 0.5rem; } \ No newline at end of file diff --git a/libs/remix-ui/solidity-uml-gen/src/lib/solidity-uml-gen.tsx b/libs/remix-ui/solidity-uml-gen/src/lib/solidity-uml-gen.tsx index c78383d529..d148c89ea2 100644 --- a/libs/remix-ui/solidity-uml-gen/src/lib/solidity-uml-gen.tsx +++ b/libs/remix-ui/solidity-uml-gen/src/lib/solidity-uml-gen.tsx @@ -1,12 +1,15 @@ -import React, { Fragment, useEffect, useState } from 'react' +import React, { Fragment, useCallback, useEffect, useState } from 'react' import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch' import { ThemeSummary } from '../types' +import UmlDownload from './components/UmlDownload' import './css/solidity-uml-gen.css' +import { UmlDownloadContext, UmlFileType } from './utilities/UmlDownloadStrategy' export interface RemixUiSolidityUmlGenProps { updatedSvg?: string loading?: boolean themeSelected?: string themeName: string + fileName: string themeCollection: ThemeSummary[] } @@ -18,24 +21,32 @@ interface ActionButtonsProps { } } - - - -export function RemixUiSolidityUmlGen ({ updatedSvg, loading }: RemixUiSolidityUmlGenProps) { +let umlCopy = '' +export function RemixUiSolidityUmlGen ({ updatedSvg, loading, fileName }: RemixUiSolidityUmlGenProps) { const [showViewer, setShowViewer] = useState(false) const [validSvg, setValidSvg] = useState(false) + const umlDownloader = new UmlDownloadContext() useEffect(() => { + if (updatedSvg.startsWith(' { + if (umlCopy.length === 0) { + return + } + umlDownloader.download(umlCopy, fileName, fileType) + }, [updatedSvg, fileName]) + function ActionButtons({ actions: { zoomIn, zoomOut, resetTransform }}: ActionButtonsProps) { return ( @@ -46,29 +57,24 @@ export function RemixUiSolidityUmlGen ({ updatedSvg, loading }: RemixUiSolidityU style={{ zIndex: 3, top: "10", right: "2em" }} >
- +
diff --git a/libs/remix-ui/solidity-uml-gen/src/lib/utilities/UmlDownloadStrategy.ts b/libs/remix-ui/solidity-uml-gen/src/lib/utilities/UmlDownloadStrategy.ts new file mode 100644 index 0000000000..72920d96d4 --- /dev/null +++ b/libs/remix-ui/solidity-uml-gen/src/lib/utilities/UmlDownloadStrategy.ts @@ -0,0 +1,86 @@ +interface IUmlDownloadStrategy { + download (uml: string, fileName: string): void +} + +export type UmlFileType = 'pdf' | 'png' + +class PdfUmlDownloadStrategy implements IUmlDownloadStrategy { + + public download (uml: string, fileName: string): void { + const svg = new Blob([uml], { type: 'image/svg+xml;charset=utf-8' }) + const Url = window.URL || window.webkitURL + const url = Url.createObjectURL(svg) + const img = document.createElement('img') + let doc + img.onload = async () => { + const canvas = document.createElement('canvas') + canvas.width = img.naturalWidth + canvas.height = img.naturalHeight + const ctx = canvas.getContext('2d') + const scale = window.devicePixelRatio*1 + canvas.style.width = `${Math.round(img.naturalWidth/scale)}`.concat('px') + canvas.style.height = `${Math.round(img.naturalHeight/scale)}`.concat('px') + canvas.style.margin = '0' + canvas.style.padding = '0' + ctx.scale(window.devicePixelRatio, window.devicePixelRatio) + ctx.drawImage(img, 0, 0, Math.round(img.naturalWidth/scale), Math.round(img.naturalHeight/scale)) + if (doc === null || doc === undefined) { + const { default: jsPDF } = await import('jspdf') + doc = new jsPDF('landscape', 'px', [img.naturalHeight, img.naturalWidth], true) + } + const pageWidth = doc.internal.pageSize.getWidth() + const pageHeight = doc.internal.pageSize.getHeight() + doc.addImage(canvas.toDataURL('image/png',0.5), 'PNG', 0, 0, pageWidth, pageHeight) + doc.save(fileName.split('/')[1].split('.')[0].concat('.pdf')) + } + img.src = url + doc = null + } +} + +class ImageUmlDownloadStrategy implements IUmlDownloadStrategy { + public download (uml: string, fileName: string): void { + const svg = new Blob([uml], { type: 'image/svg+xml;charset=utf-8' }) + const Url = window.URL || window.webkitURL + const url = Url.createObjectURL(svg) + const img = document.createElement('img') + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = img.naturalWidth + canvas.height = img.naturalHeight + const ctx = canvas.getContext('2d') + const scale = window.devicePixelRatio*1 + canvas.style.width = `${Math.round(img.naturalWidth/scale)}`.concat('px') + canvas.style.height = `${Math.round(img.naturalHeight/scale)}`.concat('px') + canvas.style.margin = '0' + canvas.style.padding = '0' + ctx.scale(window.devicePixelRatio, window.devicePixelRatio) + ctx.drawImage(img, 0, 0, Math.round(img.naturalWidth/scale), Math.round(img.naturalHeight/scale)) + const png = canvas.toDataURL('image/png') + const a = document.createElement('a') + a.download = fileName.split('/')[1].split('.')[0].concat('.png') + a.href = png + a.click() + } + img.src = url + } +} + +export class UmlDownloadContext { + private strategy: IUmlDownloadStrategy + + private setStrategy (strategy: IUmlDownloadStrategy): void { + this.strategy = strategy + } + + public download (uml: string, fileName: string, fileType: UmlFileType ): void { + if (fileType === 'pdf') { + this.setStrategy(new PdfUmlDownloadStrategy()) + } else if (fileType === 'png') { + this.setStrategy(new ImageUmlDownloadStrategy()) + } else { + throw new Error('Invalid file type') + } + this.strategy.download(uml, fileName) + } +} \ No newline at end of file diff --git a/package.json b/package.json index 1f90b8bbb3..3a8bee75f0 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,6 @@ "core-js": "^3.6.5", "deep-equal": "^1.0.1", "document-register-element": "1.13.1", - "dom-to-pdf": "^0.3.1", "eslint-config-prettier": "^8.5.0", "ethers": "^5.4.2", "ethjs-util": "^0.1.6", @@ -171,6 +170,7 @@ "isomorphic-git": "^1.8.2", "jquery": "^3.3.1", "js-yaml": "^4.1.0", + "jspdf": "^2.5.1", "jszip": "^3.6.0", "latest-version": "^5.1.0", "merge": "^2.1.1", @@ -189,12 +189,13 @@ "react-multi-carousel": "^2.8.2", "react-router-dom": "^6.3.0", "react-tabs": "^3.2.2", - "react-zoom-pan-pinch": "^2.2.0", + "react-zoom-pan-pinch": "^3.0.2", "regenerator-runtime": "0.13.7", "rss-parser": "^3.12.0", "signale": "^1.4.0", "sol2uml": "^2.4.3", "string-similarity": "^4.0.4", + "svg2pdf.js": "^2.2.1", "swarmgw": "^0.3.1", "time-stamp": "^2.2.0", "toml": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 77accce809..fb051fa3da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11473,18 +11473,6 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" -"dom-to-image@https://github.com/dmapper/dom-to-image": - version "2.6.0" - resolved "https://github.com/dmapper/dom-to-image#a7c386a8ea813930f05449ac71ab4be0c262dff3" - -dom-to-pdf@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/dom-to-pdf/-/dom-to-pdf-0.3.1.tgz#06db966acc73a7b81ce183202f7b6ff1f5f38578" - integrity sha512-2duxMNttyQr5XySV2t7gftCVhHr+zoE/f1h0nQDp/1KugAsk07EkZ9zcPFgZihcyj4dXBUWyVSxOik8czuFGDQ== - dependencies: - dom-to-image "https://github.com/dmapper/dom-to-image" - jspdf "^2.5.1" - dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" @@ -13344,6 +13332,11 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +font-family-papandreou@^0.2.0-patch1: + version "0.2.0-patch2" + resolved "https://registry.yarnpkg.com/font-family-papandreou/-/font-family-papandreou-0.2.0-patch2.tgz#c75b659e96ffbc7ab2af651cf7b4910b334e8dd2" + integrity sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw== + for-each@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -21944,10 +21937,10 @@ react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" -react-zoom-pan-pinch@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-2.2.0.tgz#15dd97aef798699016e4e30182cc51c4bddd4739" - integrity sha512-khOlTeTI/ZXtbCfqUmkKW0HpM+w0RklEQ1DlFVi0D9y90r+Z8x+ipKBXvPQC3rUu5VoYK4603SY8GsA6enfa8w== +react-zoom-pan-pinch@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.0.2.tgz#706c67e875e9a30480cdbef8dd4e3d6fdac9921c" + integrity sha512-c8BxPl/zK6RiOYrV/xBQ+ebgZpsMvbz6WOoqv2P/1QWxGCk1+q3xWF+5ub4QYasv4W8+J6vSelOR8H0WCEbL4w== react@^17.0.2: version "17.0.2" @@ -23804,6 +23797,11 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +specificity@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" + integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== + split-on-first@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" @@ -24408,6 +24406,16 @@ svg-pathdata@^6.0.3: resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== +svg2pdf.js@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/svg2pdf.js/-/svg2pdf.js-2.2.1.tgz#fa81849be57c5a405c8394d35e4a4ea8aaaebeb5" + integrity sha512-gJsFT42tb+pYTuFudkKgpMws54DvsJW7wmzGRUY1b9CUJpRMoBU5B4HrCMUTlK2lpcdPL5cOyr84hy2BEj1/Ag== + dependencies: + cssesc "^3.0.0" + font-family-papandreou "^0.2.0-patch1" + specificity "^0.4.1" + svgpath "^2.3.0" + svgo@^2.7.0, svgo@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" @@ -24421,6 +24429,11 @@ svgo@^2.7.0, svgo@^2.8.0: picocolors "^1.0.0" stable "^0.1.8" +svgpath@^2.3.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.6.0.tgz#5b160ef3d742b7dfd2d721bf90588d3450d7a90d" + integrity sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg== + swarm-js@^0.1.40: version "0.1.40" resolved "https://registry.yarnpkg.com/swarm-js/-/swarm-js-0.1.40.tgz#b1bc7b6dcc76061f6c772203e004c11997e06b99"