├── doc └── .gitignore ├── screenshot.png ├── src └── main │ ├── disableUpdate.js │ ├── electron-preload.js │ └── electron.js ├── .github ├── FUNDING.yml ├── workflows │ ├── hash-gen.yml │ ├── stale.yml │ ├── electron-builder.yml │ └── electron-builder-win.yml ├── feature_request.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── bug_report.md ├── dependency-diagram.png ├── .gitmodules ├── .gitignore ├── preload.js ├── SECURITY.md ├── sync.cjs ├── appveyor.yml ├── electron-builder-snap.json ├── electron-builder-appx.json ├── electron-builder-win32.json ├── electron-builder-win-arm64.json ├── electron-builder-win.json ├── .travis.yml ├── package.json ├── electron-builder-linux-mac.json ├── CODE_OF_CONDUCT.md ├── README.md ├── DEVELOPMENT.md └── LICENSE /doc/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgraph/drawio-desktop/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/main/disableUpdate.js: -------------------------------------------------------------------------------- 1 | export function disableUpdate() 2 | { 3 | return false; 4 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jgraph 4 | -------------------------------------------------------------------------------- /dependency-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgraph/drawio-desktop/HEAD/dependency-diagram.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "drawio"] 2 | path = drawio 3 | url = https://github.com/jgraph/drawio.git 4 | branch = dev 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .project 4 | /dist/ 5 | /.classpath 6 | /.settings 7 | package-lock.json 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | console.log('in preload', __dirname) 2 | 3 | PreApp = { 4 | log: s => {console.log('PreApp:', s)}, 5 | } 6 | 7 | window.addEventListener('load', e => { 8 | PreApp.log('in onLoad') 9 | }) 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | -------- | ------------------ | 7 | | Latest | :white_check_mark: | 8 | | Older | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | If you discover a security vulnerability in axios please disclose it via our [huntr page](https://huntr.dev/repos/jgraph/drawio-desktop/). Bounty eligibility, CVE assignment, response times and past reports are all there. 13 | -------------------------------------------------------------------------------- /sync.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const appjsonpath = path.join(__dirname, 'package.json') 5 | const disableUpdatePath = path.join(__dirname, 'src/main', 'disableUpdate.js') 6 | 7 | let ver = fs.readFileSync(path.join(__dirname, 'drawio', 'VERSION'), 'utf8') 8 | //let ver = '14.1.5' // just to test autoupdate 9 | 10 | let pj = require(appjsonpath) 11 | 12 | pj.version = ver 13 | 14 | fs.writeFileSync(appjsonpath, JSON.stringify(pj, null, 2), 'utf8') 15 | //Enable/disable updates 16 | fs.writeFileSync(disableUpdatePath, 'export function disableUpdate() { return ' + (process.argv[2] == 'disableUpdate'? 'true' : 'false') + ';}', 'utf8'); 17 | -------------------------------------------------------------------------------- /.github/workflows/hash-gen.yml: -------------------------------------------------------------------------------- 1 | name: Generate sha256 hashes for release files 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Generate Hashes 12 | uses: MCJack123/ghaction-generate-release-hashes@v1 13 | with: 14 | hash-type: sha256 15 | file-name: hashes.txt 16 | - name: Upload Hashes to release 17 | uses: svenstaro/upload-release-action@v2 18 | with: 19 | repo_token: ${{ secrets.GITHUB_TOKEN }} 20 | file: hashes.txt 21 | overwrite: true 22 | asset_name: Files-SHA256-Hashes.txt 23 | tag: ${{ github.ref }} 24 | 25 | -------------------------------------------------------------------------------- /.github/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | * [ ] I agree to follow the [Code of Conduct](https://github.com/jgraph/drawio/blob/master/CODE_OF_CONDUCT.md) that this project adheres to. 8 | * [ ] I have searched the issue tracker for a feature request that matches the one I want to file, without success. 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | * [ ] I agree to follow the [Code of Conduct](https://github.com/jgraph/drawio-desktop/blob/master/CODE_OF_CONDUCT.md) that this project adheres to. 8 | * [ ] I have searched the issue tracker for a feature request that matches the one I want to file, without success. 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | 3 | platform: 4 | - x64 5 | 6 | cache: 7 | 8 | init: 9 | - git config --global core.autocrlf input 10 | 11 | clone_script: 12 | - cmd: git clone --depth=1 -q --branch=%APPVEYOR_REPO_BRANCH% https://github.com/%APPVEYOR_REPO_NAME%.git %APPVEYOR_BUILD_FOLDER% 13 | - cmd: cd %APPVEYOR_BUILD_FOLDER% 14 | - cmd: git checkout -qf %APPVEYOR_REPO_COMMIT% 15 | - ps: (gc .\.gitmodules) -replace 'git@github.com:','https://github.com/' | Out-File -encoding ASCII .gitmodules 16 | - cmd: git submodule update --init --recursive 17 | 18 | 19 | install: 20 | - ps: Install-Product node 10 x64 21 | - npm install -g yarn 22 | - yarn install 23 | - cd drawio/src/main/webapp 24 | - yarn install 25 | - cd ../.. 26 | 27 | before_build: 28 | 29 | build_script: 30 | - yarn run sync 31 | - yarn run release-win 32 | - yarn run sync disableUpdate 33 | - yarn run release-appx 34 | 35 | test: off 36 | -------------------------------------------------------------------------------- /electron-builder-snap.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.jgraph.drawio.desktop", 3 | "copyright": "Copyright 2017-2025 draw.io", 4 | "asar": true, 5 | "files": [ 6 | "**/*", 7 | "!**/WEB-INF{,/**}" 8 | ], 9 | "artifactName": "${productName}-${arch}-${version}.${ext}", 10 | "directories": { 11 | "output": "./dist/" 12 | }, 13 | "npmRebuild": false, 14 | "linux": { 15 | "executableName": "drawio", 16 | "category": "Graphics", 17 | "maintainer": "draw.io ", 18 | "icon": "./build", 19 | "target": [ 20 | "snap" 21 | ] 22 | }, 23 | "snap": { 24 | "base": "core20", 25 | "plugs": [ 26 | "default", 27 | "removable-media" 28 | ] 29 | }, 30 | "afterPack": "build/fuses.cjs", 31 | "fileAssociations": [ 32 | { 33 | "ext": "drawio", 34 | "name": "draw.io Diagram", 35 | "description": "draw.io Diagram", 36 | "mimeType": "application/vnd.jgraph.mxfile", 37 | "role": "Editor" 38 | }, 39 | { 40 | "ext": "vsdx", 41 | "name": "VSDX Document", 42 | "description": "VSDX Document", 43 | "mimeType": "application/vnd.visio", 44 | "role": "Editor" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /electron-builder-appx.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.jgraph.drawio.desktop", 3 | "copyright": "Copyright 2017-2019 draw.io", 4 | "asar": true, 5 | "files": [ 6 | "**/*", 7 | "!**/WEB-INF{,/**}" 8 | ], 9 | "artifactName": "${productName}-${arch}-${version}.${ext}", 10 | "directories": { 11 | "output": "./dist/" 12 | }, 13 | "npmRebuild": false, 14 | "publish": { 15 | "provider": "github" 16 | }, 17 | "win": { 18 | "target": [ 19 | { 20 | "target": "appx", 21 | "arch": [ 22 | "x64" 23 | ] 24 | } 25 | ] 26 | }, 27 | "appx": { 28 | "displayName": "draw.io Diagrams", 29 | "publisherDisplayName": "draw.io Ltd", 30 | "identityName": "draw.io.draw.ioDiagrams", 31 | "publisher": "CN=9E628CCB-BE04-4557-A5A8-81EC34B09733" 32 | }, 33 | "afterPack": "build/fuses.cjs", 34 | "fileAssociations": [ 35 | { 36 | "ext": "drawio", 37 | "name": "draw.io Diagram", 38 | "description": "draw.io Diagram", 39 | "mimeType": "application/vnd.jgraph.mxfile", 40 | "role": "Editor" 41 | }, 42 | { 43 | "ext": "vsdx", 44 | "name": "VSDX Document", 45 | "description": "VSDX Document", 46 | "mimeType": "application/vnd.visio", 47 | "role": "Editor" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /electron-builder-win32.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.jgraph.drawio.desktop", 3 | "copyright": "Copyright 2017-2025 draw.io Ltd", 4 | "asar": true, 5 | "files": [ 6 | "**/*", 7 | "!**/WEB-INF{,/**}" 8 | ], 9 | "directories": { 10 | "output": "./dist/" 11 | }, 12 | "npmRebuild": false, 13 | "publish": { 14 | "provider": "github" 15 | }, 16 | "win": { 17 | "target": [ 18 | { 19 | "target": "nsis", 20 | "arch": [ 21 | "ia32" 22 | ] 23 | } 24 | ] 25 | }, 26 | "nsis": { 27 | "artifactName": "${productName}-ia32-${version}-windows-32bit-installer.${ext}", 28 | "oneClick": false, 29 | "perMachine": true, 30 | "allowToChangeInstallationDirectory": true, 31 | "runAfterFinish": false, 32 | "createDesktopShortcut": false 33 | }, 34 | "afterPack": "build/fuses.cjs", 35 | "fileAssociations": [ 36 | { 37 | "ext": "drawio", 38 | "name": "draw.io Diagram", 39 | "description": "draw.io Diagram", 40 | "mimeType": "application/vnd.jgraph.mxfile", 41 | "role": "Editor" 42 | }, 43 | { 44 | "ext": "vsdx", 45 | "name": "VSDX Document", 46 | "description": "VSDX Document", 47 | "mimeType": "application/vnd.visio", 48 | "role": "Editor" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | ### Preflight Checklist 8 | 9 | 10 | * [ ] I agree to follow the [Code of Conduct](https://github.com/jgraph/drawio-desktop/blob/master/CODE_OF_CONDUCT.md) that this project adheres to. 11 | * [ ] I have searched the issue tracker for a feature request that matches the one I want to file, without success. 12 | 13 | You must agree to search and the code of conduct. You must fill in this entire template. If you delete part/all or miss parts out your issue will be closed. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Screenshots** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **draw.io version (In the Help->About menu of the draw.io editor):** 32 | 33 | - draw.io version x.y.z 34 | 35 | **Desktop (please complete the following information):** 36 | - OS: Windows, MacOS, Linux... 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.github/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | ### Preflight Checklist 8 | 9 | 10 | * [ ] I agree to follow the [Code of Conduct](https://github.com/jgraph/drawio/blob/master/CODE_OF_CONDUCT.md) that this project adheres to. 11 | * [ ] I have searched the issue tracker for a feature request that matches the one I want to file, without success. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **draw.io version (In the Help->About menu of the draw.io editor):** 30 | 31 | - draw.io version [e.g. 12.6.7] 32 | 33 | **Desktop (please complete the following information):** 34 | - OS: [e.g. iOS] 35 | - Browser [e.g. chrome, safari] 36 | - Version [e.g. 22] 37 | 38 | **Smartphone (please complete the following information):** 39 | - Device: [e.g. iPhone6] 40 | - OS: [e.g. iOS8.1] 41 | - Browser [e.g. stock browser, safari] 42 | - Version [e.g. 22] 43 | 44 | **Additional context** 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '42 3 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. See [the FAQ](https://github.com/jgraph/drawio/wiki/Stale-bot-FAQ) for more information.' 25 | stale-pr-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. See [the FAQ](https://github.com/jgraph/drawio/wiki/Stale-bot-FAQ) for more information.' 26 | stale-issue-label: 'wontfix' 27 | stale-pr-label: 'wontfix' 28 | close-issue-label: 'declined' 29 | days-before-stale: 250 30 | days-before-close: 14 31 | exempt-issue-labels: notstale 32 | exempt-pr-labels: notstale 33 | -------------------------------------------------------------------------------- /electron-builder-win-arm64.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.jgraph.drawio.desktop", 3 | "copyright": "Copyright 2017-2025 draw.io Ltd", 4 | "asar": true, 5 | "files": [ 6 | "**/*", 7 | "!**/WEB-INF{,/**}" 8 | ], 9 | "directories": { 10 | "output": "./dist/" 11 | }, 12 | "npmRebuild": false, 13 | "publish": { 14 | "provider": "github" 15 | }, 16 | "win": { 17 | "target": [ 18 | { 19 | "target": "nsis", 20 | "arch": [ 21 | "arm64" 22 | ] 23 | }, 24 | { 25 | "target": "portable", 26 | "arch": [ 27 | "arm64" 28 | ] 29 | } 30 | ] 31 | }, 32 | "nsis": { 33 | "artifactName": "${productName}-arm64-${version}-windows-arm64-installer.${ext}", 34 | "oneClick": false, 35 | "perMachine": true, 36 | "allowToChangeInstallationDirectory": true, 37 | "runAfterFinish": false, 38 | "createDesktopShortcut": false 39 | }, 40 | "portable": { 41 | "artifactName": "${productName}-arm64-${version}-windows-arm64-no-installer.${ext}" 42 | }, 43 | "afterPack": "build/fuses.cjs", 44 | "fileAssociations": [ 45 | { 46 | "ext": "drawio", 47 | "name": "draw.io Diagram", 48 | "description": "draw.io Diagram", 49 | "mimeType": "application/vnd.jgraph.mxfile", 50 | "role": "Editor" 51 | }, 52 | { 53 | "ext": "vsdx", 54 | "name": "VSDX Document", 55 | "description": "VSDX Document", 56 | "mimeType": "application/vnd.visio", 57 | "role": "Editor" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /electron-builder-win.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.jgraph.drawio.desktop", 3 | "copyright": "Copyright 2017-2025 draw.io Ltd", 4 | "asar": true, 5 | "files": [ 6 | "**/*", 7 | "!**/WEB-INF{,/**}" 8 | ], 9 | "directories": { 10 | "output": "./dist/" 11 | }, 12 | "npmRebuild": false, 13 | "publish": { 14 | "provider": "github" 15 | }, 16 | "win": { 17 | "target": [ 18 | { 19 | "target": "nsis", 20 | "arch": [ 21 | "x64" 22 | ] 23 | }, 24 | { 25 | "target": "msi", 26 | "arch": [ 27 | "x64" 28 | ] 29 | } 30 | ] 31 | }, 32 | "nsis": { 33 | "artifactName": "${productName}-${version}-windows-installer.${ext}", 34 | "oneClick": false, 35 | "perMachine": true, 36 | "allowToChangeInstallationDirectory": true, 37 | "runAfterFinish": false, 38 | "createDesktopShortcut": false 39 | }, 40 | "msi": { 41 | "artifactName": "${productName}-${version}.${ext}", 42 | "runAfterFinish": false, 43 | "createDesktopShortcut": false 44 | }, 45 | "afterPack": "build/fuses.cjs", 46 | "fileAssociations": [ 47 | { 48 | "ext": "drawio", 49 | "name": "draw.io Diagram", 50 | "description": "draw.io Diagram", 51 | "mimeType": "application/vnd.jgraph.mxfile", 52 | "role": "Editor" 53 | }, 54 | { 55 | "ext": "vsdx", 56 | "name": "VSDX Document", 57 | "description": "VSDX Document", 58 | "mimeType": "application/vnd.visio", 59 | "role": "Editor" 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/main/electron-preload.js: -------------------------------------------------------------------------------- 1 | const { 2 | contextBridge, 3 | ipcRenderer 4 | } = require("electron"); 5 | 6 | let reqId = 1; 7 | let reqInfo = {}; 8 | let fileChangedListeners = {}; 9 | 10 | ipcRenderer.on('mainResp', (event, resp) => 11 | { 12 | var callbacks = reqInfo[resp.reqId]; 13 | 14 | if (resp.error) 15 | { 16 | callbacks.error(resp.msg, resp.e); 17 | } 18 | else 19 | { 20 | callbacks.callback(resp.data); 21 | } 22 | 23 | delete reqInfo[resp.reqId]; 24 | }); 25 | 26 | ipcRenderer.on('fileChanged', (event, resp) => 27 | { 28 | var listener = fileChangedListeners[resp.path]; 29 | 30 | if (listener) 31 | { 32 | listener(resp.curr, resp.prev); 33 | } 34 | }); 35 | 36 | contextBridge.exposeInMainWorld( 37 | 'electron', { 38 | request: (msg, callback, error) => 39 | { 40 | msg.reqId = reqId++; 41 | reqInfo[msg.reqId] = {callback: callback, error: error}; 42 | 43 | //TODO Maybe a special function for this better than this hack? 44 | //File watch special case where the callback is called multiple times 45 | if (msg.action == 'watchFile') 46 | { 47 | fileChangedListeners[msg.path] = msg.listener; 48 | delete msg.listener; 49 | } 50 | 51 | ipcRenderer.send('rendererReq', msg); 52 | }, 53 | registerMsgListener: function(action, callback) 54 | { 55 | ipcRenderer.on(action, function(event, args) 56 | { 57 | callback(args); 58 | }); 59 | }, 60 | sendMessage: function(action, args) 61 | { 62 | ipcRenderer.send(action, args); 63 | }, 64 | listenOnce: function(action, callback) 65 | { 66 | ipcRenderer.once(action, function(event, args) 67 | { 68 | callback(args); 69 | }); 70 | } 71 | } 72 | ); 73 | 74 | contextBridge.exposeInMainWorld( 75 | 'process', { 76 | type: process.type, 77 | versions: process.versions 78 | } 79 | ); 80 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode11.3 2 | 3 | dist: bionic 4 | 5 | language: c 6 | 7 | matrix: 8 | include: 9 | - os: osx 10 | osx_image: xcode11.3 11 | - os: linux 12 | env: CC=clang CXX=clang++ npm_config_clang=1 13 | compiler: clang 14 | 15 | cache: 16 | 17 | addons: 18 | apt: 19 | packages: 20 | - libgnome-keyring-dev 21 | - icnsutils 22 | - graphicsmagick 23 | - xz-utils 24 | - rpm 25 | 26 | # Handle git submodules yourself 27 | git: 28 | submodules: false 29 | 30 | before_install: 31 | # - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.5.5/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.5.5.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 32 | - curl -o- -L https://yarnpkg.com/install.sh | bash 33 | - export PATH="$HOME/.yarn/bin:$PATH" 34 | # Use sed to replace the SSH URL with the public URL, then initialize submodules 35 | - sed -ie 's/git@github.com:/https:\/\/github.com\//' .gitmodules 36 | - git submodule update --init --recursive 37 | - npm install -g yarn 38 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then echo $SNAP_TOKEN > /tmp/login_token_file; fi 39 | 40 | install: 41 | - nvm install 10 42 | - yarn install 43 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo snap install snapcraft --classic; fi 44 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then snapcraft login --with /tmp/login_token_file; fi 45 | 46 | script: 47 | - yarn run sync 48 | - yarn run release-linux 49 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then yarn run release-snap; fi 50 | # Cannot configure electron-builder to publish to stable channel, so do it explicitly 51 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then snapcraft push --release stable dist/draw.io-amd64-*.snap; fi 52 | branches: 53 | except: 54 | - "/^v\\d+\\.\\d+\\.\\d+$/" 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draw.io", 3 | "version": "29.0.3", 4 | "description": "draw.io desktop", 5 | "exports": "./src/main/electron.js", 6 | "type": "module", 7 | "main": "src/main/electron.js", 8 | "engines": { 9 | "node": ">=20" 10 | }, 11 | "scripts": { 12 | "start": "electron .", 13 | "sync": "node ./sync.cjs", 14 | "release-win": "electron-builder --config electron-builder-win.json --publish always", 15 | "release-win32": "electron-builder --config electron-builder-win32.json --publish always", 16 | "release-win-arm64": "electron-builder --config electron-builder-win-arm64.json --publish always", 17 | "release-appx": "electron-builder --config electron-builder-appx.json --publish always", 18 | "release-linux": "electron-builder --config electron-builder-linux-mac.json --publish always", 19 | "release-snap": "electron-builder --config electron-builder-snap.json --publish never" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git@github.com:jgraph/drawio-desktop.git" 24 | }, 25 | "keywords": [ 26 | "draw.io", 27 | "diagram", 28 | "flowchart", 29 | "UML" 30 | ], 31 | "author": "JGraph ", 32 | "license": "Apache-2.0", 33 | "bugs": { 34 | "url": "https://github.com/jgraph/drawio-desktop/issues" 35 | }, 36 | "homepage": "https://github.com/jgraph/drawio", 37 | "dependencies": { 38 | "@cantoo/pdf-lib": "^2.5.3", 39 | "commander": "^13.1.0", 40 | "compression": "^1.8.1", 41 | "crc": "^4.3.2", 42 | "electron-context-menu": "^4.1.0", 43 | "electron-log": "^5.4.3", 44 | "electron-progressbar": "^2.2.1", 45 | "electron-store": "^10.1.0", 46 | "electron-updater": "^6.6.8", 47 | "tslib": "^2.8.1" 48 | }, 49 | "devDependencies": { 50 | "@electron/fuses": "^1.8.0", 51 | "@electron/notarize": "^3.1.0", 52 | "dotenv": "^16.6.1", 53 | "electron": "^38.7.0", 54 | "electron-builder": "^26.0.20", 55 | "sumchecker": "^3.0.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /electron-builder-linux-mac.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.jgraph.drawio.desktop", 3 | "copyright": "Copyright 2017-2019 draw.io", 4 | "asar": true, 5 | "files": [ 6 | "**/*", 7 | "!**/WEB-INF{,/**}" 8 | ], 9 | "artifactName": "${productName}-${arch}-${version}.${ext}", 10 | "directories": { 11 | "output": "./dist/" 12 | }, 13 | "npmRebuild": false, 14 | "publish": { 15 | "provider": "github" 16 | }, 17 | "mac": { 18 | "hardenedRuntime": true, 19 | "gatekeeperAssess": false, 20 | "entitlements": "build/entitlements.mac.plist", 21 | "entitlementsInherit": "build/entitlements.mac.plist", 22 | "category": "public.app-category.graphics-design", 23 | "target": [ 24 | { "target": "zip", "arch": [ 25 | "x64", 26 | "arm64" 27 | ] 28 | }, 29 | { "target": "dmg", "arch": [ 30 | "x64", 31 | "arm64", 32 | "universal" 33 | ] 34 | } 35 | ] 36 | }, 37 | "afterSign": "build/notarize.mjs", 38 | "afterPack": "build/fuses.cjs", 39 | "dmg": { 40 | }, 41 | "linux": { 42 | "executableName": "drawio", 43 | "category": "Graphics", 44 | "maintainer": "JGraph ", 45 | "icon": "./build", 46 | "target": [ 47 | { "target": "AppImage", "arch": [ 48 | "x64", 49 | "arm64" 50 | ] 51 | }, 52 | { "target": "deb", "arch": [ 53 | "x64", 54 | "arm64" 55 | ] 56 | }, 57 | { "target": "rpm", "arch": [ 58 | "x64", 59 | "arm64" 60 | ] 61 | } 62 | ] 63 | }, 64 | "rpm": { 65 | "fpm": [ 66 | "--rpm-rpmbuild-define=_build_id_links none", 67 | "--rpm-digest=sha256" 68 | ] 69 | }, 70 | "fileAssociations": [ 71 | { 72 | "ext": "drawio", 73 | "name": "draw.io Diagram", 74 | "description": "draw.io Diagram", 75 | "mimeType": "application/vnd.jgraph.mxfile", 76 | "role": "Editor" 77 | }, 78 | { 79 | "ext": "vsdx", 80 | "name": "VSDX Document", 81 | "description": "VSDX Document", 82 | "mimeType": "application/vnd.visio", 83 | "role": "Editor" 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct: 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers aim to making participation in our project and our community a harassment-free experience for everyone. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment, including focusing on individual developers when a topic should be broadly addressed. 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | * Not respecting other people's time 25 | * Being impatient or rude 26 | * Pressing developers for priority fixes or ETAs 27 | * Guilting the developers into focusing on your issue(s) 28 | * Repeatedly showing an inappropriate level of entitlement, asking for issue status, or bumping issues. 29 | 30 | ## Our Responsibilities 31 | 32 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 33 | 34 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies to all JGraph projects and the draw.io google groups. 39 | 40 | Project maintainers are not subject to this code. 41 | 42 | ## Attribution 43 | 44 | This Code of Conduct is adapted from the [Contributor-Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 45 | 46 | [homepage]: https://contributor-covenant.org 47 | [version]: https://contributor-covenant.org/version/1/4/ 48 | -------------------------------------------------------------------------------- /.github/workflows/electron-builder.yml: -------------------------------------------------------------------------------- 1 | name: Electron Builder CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest] 14 | env: 15 | CC: clang 16 | CXX: clang++ 17 | npm_config_clang: 1 18 | APPLEID: ${{ secrets.APPLEID }} 19 | APPLEIDPASS: ${{ secrets.APPLEIDPASS }} 20 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 21 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 22 | CSC_LINK: ${{ secrets.CSC_LINK }} 23 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_TOKEN }} 25 | OS_NAME: ${{ matrix.os }} 26 | GH_REF: ${{ github.ref }} 27 | steps: 28 | - name: Checkout reposistory 29 | uses: actions/checkout@v4 30 | with: 31 | submodules: true 32 | - name: Checkout drawio-dev 33 | uses: actions/checkout@v4 34 | with: 35 | repository: jgraph/drawio-dev 36 | token: ${{ secrets.GH_TOKEN }} 37 | ref: release 38 | path: drawio-dev 39 | submodules: false 40 | - name: Get drawio Tag & Submodules 41 | run: | 42 | cd drawio-dev 43 | # Get the current build tag from draw.io 44 | export tmp1=${GH_REF/refs\//} 45 | export tmp2=${tmp1/\/v/\/diagramly-} 46 | export tmp3=${tmp2//\./_} 47 | git fetch origin refs/$tmp3:refs/$tmp3 --no-tags 48 | git checkout $tmp3 -b tmp-deploy 49 | cp src/main/webapp/js/*.min.js ../drawio/src/main/webapp/js/ 50 | cd .. 51 | rm -rf drawio-dev 52 | cd drawio 53 | rm -rf docs etc src/main/java src/main/webapp/connect src/main/webapp/service-worker* src/main/webapp/workbox-* 54 | cd src/main/webapp/js 55 | rm -rf atlas-viewer.min.js atlas.min.js cryptojs deflate dropbox embed* freehand integrate.min.js jquery jszip mermaid onedrive orgchart reader.min.js rough sanitizer shapes.min.js simplepeer spin viewer-static.min.js viewer.min.js 56 | - name: Installing Node 57 | uses: actions/setup-node@v4 58 | with: 59 | node-version: 22 60 | - name: Build for ${{ matrix.os}} 61 | run: | 62 | if [ "$OS_NAME" = "ubuntu-latest" ]; then sudo apt-get update && sudo apt-get install -y icnsutils graphicsmagick xz-utils rpm; fi 63 | curl -o- -L https://yarnpkg.com/install.sh | bash 64 | export PATH="$HOME/.yarn/bin:$PATH" 65 | git config --global url."https://github.com/".insteadOf "git@github.com:" 66 | sudo npm install -g yarn 67 | yarn install 68 | if [ "$OS_NAME" = "ubuntu-latest" ]; then sed -ie 's/"asar": true,/"asar": true,\n"productName": "drawio",/' electron-builder-linux-mac.json; fi 69 | yarn run sync 70 | yarn run release-linux 71 | - name: Build for Snap 72 | if: ${{ matrix.os == 'ubuntu-latest' }} 73 | run: | 74 | #To generate SNAP_TOKEN run `snapcraft export-login [FILE]` and login with your snapcraft credentials. It is used now without login 75 | sudo snap install snapcraft --classic 76 | yarn run release-snap 77 | # Cannot configure electron-builder to publish to stable channel, so do it explicitly 78 | snapcraft push --release edge dist/draw.io-amd64-*.snap 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About 2 | ----- 3 | 4 | **drawio-desktop** is a diagramming desktop app based on [Electron](https://electronjs.org/) that wraps the [core draw.io editor](https://github.com/jgraph/drawio). 5 | 6 | Download built binaries from the [releases section](https://github.com/jgraph/drawio-desktop/releases). 7 | 8 | **Can I use this app for free?** Yes, under the apache 2.0 license. If you don't change the code and accept it is provided "as-is", you can use it for any purpose. 9 | 10 | Security 11 | -------- 12 | 13 | draw.io Desktop is designed to be completely isolated from the Internet, apart from the update process. This checks github.com at startup for a newer version and downloads it from an AWS S3 bucket owned by Github. All JavaScript files are self-contained, the Content Security Policy forbids running remotely loaded JavaScript. 14 | 15 | No diagram data is ever sent externally, nor do we send any analytics about app usage externally. There is a Content Security Policy in place on the web part of the interface to ensure external transmission cannot happen, even by accident. 16 | 17 | Security and isolating the app are the primarily objectives of draw.io desktop. If you ask for anything that involves external connections enabled in the app by default, the answer will be no. 18 | 19 | Support 20 | ------- 21 | 22 | Support is provided on a reasonable business constraints basis, but without anything contractually binding. All support is provided via this repo. There is no private ticketing support for non-paying users. 23 | 24 | Purchasing draw.io for Confluence or Jira does not entitle you to commercial support for draw.io desktop. 25 | 26 | Developing 27 | ---------- 28 | 29 | **draw.io** is a git submodule of **drawio-desktop**. To get both you need to clone recursively: 30 | 31 | `git clone --recursive https://github.com/jgraph/drawio-desktop.git` 32 | 33 | To run this: 34 | 1. `npm install` (in the root directory of this repo) 35 | 2. [internal use only] export DRAWIO_ENV=dev if you want to develop/debug in dev mode. 36 | 3. `npm start` _in the root directory of this repo_ runs the app. For debugging, use `npm start --enable-logging`. 37 | 38 | Note: If a symlink is used to refer to drawio repo (instead of the submodule), then symlink the `node_modules` directory inside `drawio/src/main/webapp` also. 39 | 40 | To release: 41 | 1. Update the draw.io sub-module and push the change. Add version tag before pushing to origin. 42 | 2. Wait for the builds to complete (https://travis-ci.org/jgraph/drawio-desktop and https://ci.appveyor.com/project/davidjgraph/drawio-desktop) 43 | 3. Go to https://github.com/jgraph/drawio-desktop/releases, edit the preview release. 44 | 4. Download the windows exe and windows portable, sign them using `signtool sign /a /tr http://rfc3161timestamp.globalsign.com/advanced /td SHA256 c:/path/to/your/file.exe` 45 | 5. Re-upload signed file as `draw.io-windows-installer-x.y.z.exe` and `draw.io-windows-no-installer-x.y.z.exe` 46 | 6. Add release notes 47 | 7. Publish release 48 | 49 | *Note*: In Windows release, when using both x64 and is32 as arch, the result is one big file with both archs. This is why we split them. 50 | 51 | Local Storage and Session Storage is stored in the AppData folder: 52 | 53 | - macOS: `~/Library/Application Support/draw.io` 54 | - Windows: `C:\Users\\AppData\Roaming\draw.io\` 55 | 56 | Not open-contribution 57 | --------------------- 58 | 59 | draw.io is closed to contributions (unless a maintainer permits it, which is extremely rare). 60 | 61 | The level of complexity of this project means that even simple changes 62 | can break a _lot_ of other moving parts. The amount of testing required 63 | is far more than it first seems. If we were to receive a PR, we'd have 64 | to basically throw it away and write it how we want it to be implemented. 65 | 66 | We are grateful for community involvement, bug reports, & feature requests. We do 67 | not wish to come off as anything but welcoming, however, we've 68 | made the decision to keep this project closed to contributions for 69 | the long term viability of the project. 70 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 3 | Installers repo is: https://github.com/mediaslav/drawiodesktop 4 | 5 | build/ - resources for installer, don't change file names names there 6 | electron-builder.json - main build config 7 | sync.js - fetches version from ./draw.io/VERSION for build, and installs draw.io/war/package.json "dependencies" 8 | .travis.yml - CI config 9 | appveyor.yml - CI config 10 | draw.io/ - draw.io repo as submodule 11 | 12 | Currently CI build are activated from pushing to this repo, not from draw.io. 13 | 14 | ## High level workflow 15 | 16 | 1) You push to repo 17 | 18 | 2) git hook activates CI, and CI performs build (builder will use version from ./draw.io/VERSION, ignoring version in draw.io/war/package.json) 19 | 20 | 3) After successful build CI drafts new release in github and uploads installers, mac and linux from Travis and windows one from AppVeyor 21 | ( you'll see new draft here: https://github.com/mediaslav/drawiodesktop/releases ) 22 | 23 | 4) You wait till all installers get into draft and press "Publish release" 24 | 25 | Need to open corresponding draft in: https://github.com/mediaslav/drawiodesktop/releases 26 | Press "Edit" near relevant draft, make sure you see all needed installers uploaded by CI, and press "Publish release" below 27 | 28 | So manual work is push commit, have lunch, coffee and press "Publish" button. 29 | Travis OSX builds can spend hour(s) in queue. 30 | 31 | ## Configuring CI 32 | 33 | You must provide certain environment variables for github publishing and code signing 34 | 35 | GH_TOKEN = github token, for publishing 36 | CSC_LINK = Certificate converted to base64-encoded string, or url to cert (*.p12 or *.pfx file) 37 | CSC_KEY_PASSWORD = The password to decrypt the certificate given in CSC_LINK 38 | 39 | #### Travis CI - builds OSX and Linux installers 40 | Define vars at: relevant repo page / "More options" / "Settings" 41 | 42 | #### AppVeyor CI - Windows NSIS installer 43 | Define vars at: SETTINGS / Environment / Environment variables, Add variable 44 | 45 | ## Code signing 46 | 47 | ### Windows 48 | 49 | To sign an app on Windows, there are two types of certificates: **EV Code Signing Certificate** and 50 | **Code Signing Certificate**. Both certificates work with auto-update. The regular (and often cheaper) 51 | Code Signing Certificate shows a warning during installation that goes away once enough users installed your application 52 | and you've built up trust. 53 | 54 | For CI you can use only regular **Code Signing Certificate**, because EV Certificate is bound to a physical USB dongle. 55 | 56 | SSL certificate used for website is not suitable for signing apps. 57 | 58 | See [Get a code signing certificate](https://msdn.microsoft.com/windows/hardware/drivers/dashboard/get-a-code-signing-certificate) for Windows. 59 | And this may be useful: https://cheapsslsecurity.com/#ProtectYourCode 60 | 61 | ### MacOS 62 | 63 | Mac build requires Apple-issued Developer ID certificate, you must be member of https://developer.apple.com/programs/whats-included/ 64 | to receive one. 65 | 66 | #### How to Export Certificate on macOS 67 | 68 | 1. Open Keychain. 69 | 2. Select `login` keychain, and `My Certificates` category. 70 | 3. Select all required certificates (hint: use cmd-click to select several): 71 | * `Developer ID Application:` to sign app for macOS. 72 | * `3rd Party Mac Developer Application:` and `3rd Party Mac Developer Installer:` to sign app for MAS (Mac App Store). 73 | * `Developer ID Application:` and `Developer ID Installer` to sign app and installer for distribution outside of the Mac App Store. 74 | 75 | Please note – you can select as many certificates, as need. No restrictions on electron-builder side. 76 | All selected certificates will be imported into temporary keychain on CI server. 77 | 4. Open context menu and `Export`. 78 | 79 | To encode file to base64 (macOS/linux): `base64 -i yourFile.p12 -o envValue.txt` 80 | 81 | ## WARNING 82 | 83 | draw.io/war/package.json "dependencies" will get into installer, so please put irrelevant ones into "devDependencies", which is ignored by builder. 84 | Currently it looks like: 85 | 86 | "devDependencies": { 87 | "electron": "^1.6.3" <- we obviously don't need to pack electron inside electron as dependency 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/electron-builder-win.yml: -------------------------------------------------------------------------------- 1 | name: Electron Builder CI (WIN) 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | env: 12 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | GH_REF: ${{ github.ref }} 14 | steps: 15 | - name: Checkout reposistory 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: true 19 | - name: Checkout drawio-dev 20 | uses: actions/checkout@v4 21 | with: 22 | repository: jgraph/drawio-dev 23 | token: ${{ secrets.GH_TOKEN }} 24 | ref: release 25 | path: drawio-dev 26 | submodules: false 27 | - name: Get drawio Tag & Submodules 28 | run: | 29 | cd drawio-dev 30 | # Get the current build tag from draw.io 31 | $tmp=$Env:GH_REF -replace '/v','/diagramly-' -replace '[.]','_' 32 | $ref=$tmp+':'+$tmp 33 | git fetch origin $ref --no-tags 34 | $tmp=$tmp -replace 'refs/','' 35 | git checkout $tmp -b tmp-deploy 36 | Copy-Item -Path "src\main\webapp\js\*.min.js" -Destination "..\drawio\src\main\webapp\js\" 37 | cd .. 38 | Remove-Item 'drawio-dev' -Recurse -Force 39 | cd drawio 40 | Remove-Item 'docs','etc','src\main\java','src\main\webapp\connect','src\main\webapp\service-worker*','src\main\webapp\workbox-*' -Recurse -Force 41 | cd src\main\webapp\js 42 | Remove-Item 'atlas-viewer.min.js','atlas.min.js','cryptojs','deflate','dropbox','embed*','freehand','integrate.min.js','jquery','jszip','mermaid','onedrive','orgchart','reader.min.js','rough','sanitizer','shapes.min.js','simplepeer','spin','viewer-static.min.js','viewer.min.js' -Recurse -Force 43 | - name: Installing Node 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: 22 47 | - name: Prepare for Windows Build 48 | shell: powershell #The default shell for Windows 49 | run: | 50 | git config --global url."https://github.com/".insteadOf "git@github.com:" 51 | npm install -g yarn 52 | yarn install 53 | - name: Build for Windows (x32 & arm64) 54 | env: 55 | CSC_LINK: ${{ secrets.WIN_CSC_LINK }} 56 | CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} 57 | shell: powershell #The default shell for Windows 58 | run: | 59 | #Disable auto-update and build 32bit/arm64 first such that latest.yml is for 64bit only (64bit will overwrite 32bit one) 60 | yarn run sync disableUpdate 61 | yarn run release-win32 62 | yarn run release-win-arm64 63 | - name: Build for Windows (x64) 64 | env: 65 | CSC_LINK: ${{ secrets.WIN_CSC_LINK }} 66 | CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} 67 | shell: powershell #The default shell for Windows 68 | run: | 69 | #Enable auto-update again 70 | yarn run sync 71 | yarn run release-win 72 | - name: Build unpacked Windows x64 (for zip portable) 73 | shell: powershell 74 | run: | 75 | yarn run sync disableUpdate 76 | yarn run electron-builder --win --x64 --dir 77 | 78 | - name: Zip unpacked x64 build 79 | shell: powershell 80 | run: | 81 | $version = "${{ github.ref }}" -replace 'refs/tags/v', '' 82 | cd dist 83 | 7z a "draw.io-$version-windows.zip" ".\win-unpacked\*" 84 | 85 | - name: Build for Windows (APPX) 86 | shell: powershell #The default shell for Windows 87 | run: | 88 | #Disable auto-update for appx also 89 | yarn run sync disableUpdate 90 | yarn run release-appx 91 | 92 | - name: Install GitHub CLI 93 | shell: powershell 94 | run: | 95 | Invoke-WebRequest -Uri https://github.com/cli/cli/releases/download/v2.73.0/gh_2.73.0_windows_amd64.msi -OutFile ghcli.msi 96 | Start-Process msiexec.exe -ArgumentList '/i', 'ghcli.msi', '/quiet', '/norestart' -Wait 97 | 98 | - name: Upload portable zip to GitHub Release 99 | shell: powershell 100 | env: 101 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | run: | 103 | $version = "${{ github.ref }}" -replace 'refs/tags/v', '' 104 | gh release upload "v$version" "dist/draw.io-$version-windows.zip" --clobber 105 | 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/electron.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { promises as fsProm } from 'fs'; 3 | import path from 'path'; 4 | import url from 'url'; 5 | import {Menu as menu, shell, dialog, session, screen, 6 | clipboard, nativeImage, ipcMain, app, BrowserWindow} from 'electron'; 7 | import crc from 'crc'; 8 | import zlib from 'zlib'; 9 | import log from'electron-log'; 10 | import { program } from 'commander'; 11 | import elecUpPkg from 'electron-updater'; 12 | const {autoUpdater} = elecUpPkg; 13 | import {PDFDocument} from '@cantoo/pdf-lib'; 14 | import Store from 'electron-store'; 15 | import ProgressBar from 'electron-progressbar'; 16 | import contextMenu from 'electron-context-menu'; 17 | import {spawn} from 'child_process'; 18 | import {disableUpdate as disUpPkg} from './disableUpdate.js'; 19 | 20 | let store; 21 | 22 | try 23 | { 24 | store = new Store(); 25 | } 26 | catch (e) 27 | { 28 | console.error('Failed to initialize electron-store:', e); 29 | store = null; 30 | } 31 | 32 | const disableUpdate = disUpPkg() || 33 | process.env.DRAWIO_DISABLE_UPDATE === 'true' || 34 | process.argv.indexOf('--disable-update') !== -1 || 35 | fs.existsSync('/.flatpak-info'); //This file indicates running in flatpak sandbox 36 | const silentUpdate = !disableUpdate && (process.env.DRAWIO_SILENT_UPDATE === 'true' || 37 | process.argv.indexOf('--silent-update') !== -1); 38 | autoUpdater.logger = log 39 | autoUpdater.logger.transports.file.level = 'error' 40 | autoUpdater.logger.transports.console.level = 'error' 41 | autoUpdater.autoDownload = silentUpdate 42 | autoUpdater.autoInstallOnAppQuit = silentUpdate 43 | 44 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 45 | 46 | //Command option to disable hardware acceleration 47 | if (process.argv.indexOf('--disable-acceleration') !== -1) 48 | { 49 | app.disableHardwareAcceleration(); 50 | } 51 | 52 | // Configure context menu for text fields 53 | contextMenu({ 54 | showCopyImage: true, 55 | showSaveImage: true, 56 | showSaveImageAs: true, 57 | showLookUpSelection: false, 58 | showSearchWithGoogle: false, 59 | showCopyLink: false, 60 | showSelectAll: true, 61 | append: (defaultActions, params, browserWindow) => [ 62 | { 63 | label: 'Paste and Match Style', 64 | // Only show this item when there's a text in the clipboard 65 | visible: clipboard.availableFormats().includes('text/plain'), 66 | click: () => { 67 | // Execute the paste command in the focused window 68 | browserWindow.webContents.pasteAndMatchStyle(); 69 | } 70 | } 71 | ] 72 | }); 73 | 74 | const __DEV__ = process.env.DRAWIO_ENV === 'dev' 75 | 76 | let windowsRegistry = [] 77 | let cmdQPressed = false 78 | let firstWinLoaded = false 79 | let firstWinFilePath = null 80 | const isMac = process.platform === 'darwin' 81 | const isWin = process.platform === 'win32' 82 | let enableSpellCheck = store != null ? store.get('enableSpellCheck') : false; 83 | enableSpellCheck = enableSpellCheck != null ? enableSpellCheck : isMac; 84 | let enableStoreBkp = store != null ? (store.get('enableStoreBkp') != null ? store.get('enableStoreBkp') : true) : false; 85 | let dialogOpen = false; 86 | let enablePlugins = false; 87 | const codeDir = path.join(__dirname, '/../../drawio/src/main/webapp'); 88 | const codeUrl = url.pathToFileURL(codeDir).href.replace(/\/.\:\//, str => str.toUpperCase()); // Fix for windows drive letter 89 | // Production app uses asar archive, so we need to go up two more level. It's extra cautious since asar is read-only anyway. 90 | const appBaseDir = path.join(__dirname, __dirname.endsWith(path.join('resources', 'app.asar', 'src', 'main')) ? 91 | '/../../../../' : '/../../'); 92 | let appZoom = 1; 93 | // Disabled by default 94 | let isGoogleFontsEnabled = store != null ? (store.get('isGoogleFontsEnabled') != null? store.get('isGoogleFontsEnabled') : false) : false; 95 | 96 | //Read config file 97 | var queryObj = { 98 | 'dev': __DEV__ ? 1 : 0, 99 | 'test': __DEV__ ? 1 : 0, 100 | 'gapi': 0, 101 | 'db': 0, 102 | 'od': 0, 103 | 'gh': 0, 104 | 'gl': 0, 105 | 'tr': 0, 106 | 'browser': 0, 107 | 'picker': 0, 108 | 'mode': 'device', 109 | 'export': 'https://convert.diagrams.net/node/export', 110 | 'disableUpdate': disableUpdate? 1 : 0, 111 | 'enableSpellCheck': enableSpellCheck? 1 : 0, 112 | 'enableStoreBkp': enableStoreBkp? 1 : 0, 113 | 'isGoogleFontsEnabled': isGoogleFontsEnabled? 1 : 0 114 | }; 115 | 116 | try 117 | { 118 | if (fs.existsSync(process.cwd() + '/urlParams.json')) 119 | { 120 | let urlParams = JSON.parse(fs.readFileSync(process.cwd() + '/urlParams.json')); 121 | 122 | for (var param in urlParams) 123 | { 124 | queryObj[param] = urlParams[param]; 125 | } 126 | } 127 | } 128 | catch(e) 129 | { 130 | console.log('Error in urlParams.json file: ' + e.message); 131 | } 132 | 133 | // Trying sandboxing the renderer for more protection 134 | //app.enableSandbox(); // This maybe the reason snap stopped working 135 | 136 | // Only allow request from the app code itself 137 | function validateSender (frame) 138 | { 139 | return frame.url.replace(/\/.\:\//, str => str.toUpperCase()).startsWith(codeUrl); 140 | } 141 | 142 | function isWithinDisplayBounds(pos) 143 | { 144 | const displays = screen.getAllDisplays(); 145 | 146 | return displays.reduce((result, display) => 147 | { 148 | const area = display.workArea 149 | return ( 150 | result || 151 | (pos.x >= area.x && 152 | pos.y >= area.y && 153 | pos.x < area.x + area.width && 154 | pos.y < area.y + area.height) 155 | ) 156 | }, false) 157 | } 158 | 159 | function createWindow (opt = {}) 160 | { 161 | let lastWinSizeStr = (store && store.get('lastWinSize')) || '1200,800,0,0,false,false'; 162 | let lastWinSize = lastWinSizeStr ? lastWinSizeStr.split(',') : [1200, 800]; 163 | 164 | // TODO On some Mac OS, double click the titlebar set incorrect window size 165 | if (lastWinSize[0] < 500) 166 | { 167 | lastWinSize[0] = 500; 168 | } 169 | 170 | if (lastWinSize[1] < 500) 171 | { 172 | lastWinSize[1] = 500; 173 | } 174 | 175 | let options = Object.assign( 176 | { 177 | backgroundColor: '#FFF', 178 | width: parseInt(lastWinSize[0]), 179 | height: parseInt(lastWinSize[1]), 180 | icon: `${codeDir}/images/drawlogo256.png`, 181 | webviewTag: false, 182 | webSecurity: true, 183 | webPreferences: { 184 | preload: `${__dirname}/electron-preload.js`, 185 | spellcheck: enableSpellCheck, 186 | contextIsolation: true, 187 | disableBlinkFeatures: 'Auxclick' // Is this needed? 188 | } 189 | }, opt) 190 | 191 | if (lastWinSize[2] != null) 192 | { 193 | options.x = parseInt(lastWinSize[2]); 194 | } 195 | 196 | if (lastWinSize[3] != null) 197 | { 198 | options.y = parseInt(lastWinSize[3]); 199 | } 200 | 201 | if (!isWithinDisplayBounds(options)) 202 | { 203 | options.x = null; 204 | options.y = null; 205 | } 206 | 207 | let mainWindow = new BrowserWindow(options) 208 | windowsRegistry.push(mainWindow) 209 | 210 | if (lastWinSize[4] === 'true') 211 | { 212 | mainWindow.maximize() 213 | } 214 | 215 | if (lastWinSize[5] === 'true') 216 | { 217 | mainWindow.setFullScreen(true); 218 | } 219 | 220 | if (__DEV__) 221 | { 222 | console.log('createWindow', opt) 223 | } 224 | 225 | //Cannot be read before app is ready 226 | queryObj['appLang'] = app.getLocale(); 227 | 228 | let ourl = url.format( 229 | { 230 | pathname: `${codeDir}/index.html`, 231 | protocol: 'file:', 232 | query: queryObj, 233 | slashes: true 234 | }) 235 | 236 | mainWindow.loadURL(ourl) 237 | 238 | // Open the DevTools. 239 | if (__DEV__) 240 | { 241 | mainWindow.webContents.openDevTools() 242 | } 243 | 244 | ipcMain.on('openDevTools', function(e) 245 | { 246 | if (!validateSender(e.senderFrame)) return null; 247 | 248 | mainWindow.webContents.openDevTools(); 249 | }); 250 | 251 | function rememberWinSize(win) 252 | { 253 | if (store != null) 254 | { 255 | const size = win.getSize(); 256 | const pos = win.getPosition(); 257 | store.set('lastWinSize', size[0] + ',' + size[1] + ',' + pos[0] + ',' + pos[1] + ',' + win.isMaximized() + ',' + win.isFullScreen()); 258 | } 259 | } 260 | 261 | mainWindow.on('maximize', function() 262 | { 263 | mainWindow.webContents.send('maximize') 264 | }); 265 | 266 | mainWindow.on('unmaximize', function() 267 | { 268 | mainWindow.webContents.send('unmaximize') 269 | }); 270 | 271 | mainWindow.on('resize', function() 272 | { 273 | mainWindow.webContents.send('resize') 274 | }); 275 | 276 | let uniqueIsModifiedId, modifiedModalOpen = false; 277 | 278 | ipcMain.on('isModified-result', async (e, data) => 279 | { 280 | if (!validateSender(e.senderFrame) || uniqueIsModifiedId != data.uniqueId || modifiedModalOpen) return null; 281 | 282 | if (data.isModified) 283 | { 284 | modifiedModalOpen = true; 285 | // Can't use async function here because it crashes on Linux when win.destroy is called 286 | let response = dialog.showMessageBoxSync( 287 | mainWindow, 288 | { 289 | type: 'question', 290 | buttons: ['Cancel', 'Discard Changes'], 291 | title: 'Confirm', 292 | message: 'The document has unsaved changes. Do you really want to quit without saving?' //mxResources.get('allChangesLost') 293 | }); 294 | 295 | if (response === 1) 296 | { 297 | //If user chose not to save, remove the draft 298 | if (data.draftPath != null) 299 | { 300 | await deleteFile(data.draftPath); 301 | mainWindow.destroy(); 302 | } 303 | else 304 | { 305 | mainWindow.webContents.send('removeDraft'); 306 | 307 | ipcMain.once('draftRemoved', (e) => 308 | { 309 | if (!validateSender(e.senderFrame)) return null; 310 | 311 | mainWindow.destroy(); 312 | }); 313 | } 314 | } 315 | else 316 | { 317 | cmdQPressed = false; 318 | modifiedModalOpen = false; 319 | } 320 | } 321 | else 322 | { 323 | mainWindow.destroy(); 324 | } 325 | }); 326 | 327 | mainWindow.on('close', (event) => 328 | { 329 | uniqueIsModifiedId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); 330 | 331 | if (__DEV__) 332 | { 333 | const index = windowsRegistry.indexOf(mainWindow) 334 | console.log('Window on close', index, uniqueIsModifiedId) 335 | } 336 | 337 | const contents = mainWindow.webContents 338 | 339 | if (contents != null) 340 | { 341 | contents.send('isModified', uniqueIsModifiedId); 342 | event.preventDefault(); 343 | } 344 | 345 | rememberWinSize(mainWindow); 346 | }) 347 | 348 | // Emitted when the window is closed. 349 | mainWindow.on('closed', () => 350 | { 351 | const index = windowsRegistry.indexOf(mainWindow) 352 | 353 | if (__DEV__) 354 | { 355 | console.log('Window closed idx:%d', index) 356 | } 357 | 358 | windowsRegistry.splice(index, 1) 359 | }) 360 | 361 | return mainWindow 362 | } 363 | 364 | function isPluginsEnabled() 365 | { 366 | return enablePlugins; 367 | } 368 | // This method will be called when Electron has finished 369 | // initialization and is ready to create browser windows. 370 | // Some APIs can only be used after this event occurs. 371 | app.whenReady().then(() => 372 | { 373 | // Enforce our CSP on all contents 374 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => 375 | { 376 | callback({ 377 | responseHeaders: { 378 | ...details.responseHeaders, 379 | // Replace the first sha with the one of the current version shown in the console log (the second one is for the second script block which is rarely changed) 380 | // 3rd sha is for electron-progressbar 381 | 'Content-Security-Policy': ['default-src \'self\'; script-src \'self\' \'sha256-f6cHSTUnCvbQqwa6rKcbWIpgN9dLl0ROfpEKTQUQPr8=\' \'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=\' \'sha256-ZQ86kVKhLmcnklYAnUksoyZaLkv7vvOG9cc/hBJAEuQ=\'; connect-src \'self\'' + 382 | (isGoogleFontsEnabled? ' https://fonts.googleapis.com https://fonts.gstatic.com' : '') + '; img-src * data:; media-src *; font-src * data:; frame-src \'none\'; style-src \'self\' \'unsafe-inline\'' + 383 | (isGoogleFontsEnabled? ' https://fonts.googleapis.com' : '') + '; base-uri \'none\';child-src \'self\';object-src \'none\';'] 384 | } 385 | }) 386 | }); 387 | 388 | const pluginsCodeUrl = url.pathToFileURL(path.join(getAppDataFolder(), '/plugins/')).href.replace(/\/.\:\//, str => str.toUpperCase()); 389 | 390 | // Enforce loading file only from our app directory 391 | session.defaultSession.webRequest.onBeforeRequest({urls: ['file://*']}, (details, callback) => 392 | { 393 | const url = details.url.replace(/\/.\:\//, str => str.toUpperCase()); 394 | 395 | if (!url.startsWith(codeUrl) && (!isPluginsEnabled() || (isPluginsEnabled() && !url.startsWith(pluginsCodeUrl)))) 396 | { 397 | console.log('Blocked loading file from ' + details.url, url, codeUrl, pluginsCodeUrl); 398 | callback({cancel: true}); 399 | } 400 | else 401 | { 402 | callback({}); 403 | } 404 | }); 405 | 406 | ipcMain.on('newfile', (e, arg) => 407 | { 408 | if (!validateSender(e.senderFrame)) return null; 409 | 410 | let opts = {}; 411 | 412 | if (arg) 413 | { 414 | if (arg.width) 415 | { 416 | opts.width = arg.width; 417 | } 418 | 419 | if (arg.height) 420 | { 421 | opts.height = arg.height; 422 | } 423 | } 424 | 425 | createWindow(opts); 426 | }) 427 | 428 | let argv = process.argv 429 | 430 | // https://github.com/electron/electron/issues/4690#issuecomment-217435222 431 | if (process.defaultApp != true) 432 | { 433 | argv.unshift(null) 434 | } 435 | 436 | var validFormatRegExp = /^(pdf|svg|png|jpeg|jpg|xml)$/; 437 | var themeRegExp = /^(dark|light)$/; 438 | var linkTargetRegExp = /^(auto|new-win|same-win)$/; 439 | 440 | function argsRange(val) 441 | { 442 | return val.split('..').map(n => parseInt(n, 10) - 1); 443 | } 444 | 445 | try 446 | { 447 | program.allowExcessArguments(); 448 | program 449 | .version(app.getVersion()) 450 | .usage('[options] ') 451 | .argument('[input file/folder]', 'input drawio file or a folder with drawio files') 452 | .allowUnknownOption() //-h and --help are considered unknown!! 453 | .option('-c, --create', 'creates a new empty file if no file is passed') 454 | .option('-k, --check', 'does not overwrite existing files') 455 | .option('-x, --export', 'export the input file/folder based on the given options') 456 | .option('-r, --recursive', 'for a folder input, recursively convert all files in sub-folders also') 457 | .option('-o, --output ', 'specify the output file/folder. If omitted, the input file name is used for output with the specified format as extension') 458 | .option('-f, --format ', 459 | 'if output file name extension is specified, this option is ignored (file type is determined from output extension, possible export formats are pdf, png, jpg, svg, and xml)', 460 | validFormatRegExp, 'pdf') 461 | .option('-q, --quality ', 462 | 'output image quality for JPEG (default: 90)', parseInt) 463 | .option('-t, --transparent', 464 | 'set transparent background for PNG') 465 | .option('-e, --embed-diagram', 466 | 'includes a copy of the diagram (for PNG, SVG and PDF formats only)') 467 | .option('--embed-svg-images', 468 | 'Embed Images in SVG file (for SVG format only)') 469 | .option('--embed-svg-fonts ', 470 | 'Embed Fonts in SVG file (for SVG format only). Default is true', function(x){return x === 'true'}, true) 471 | .option('-b, --border ', 472 | 'sets the border width around the diagram (default: 0)', parseInt) 473 | .option('-s, --scale ', 474 | 'scales the diagram size', parseFloat) 475 | .option('--width ', 476 | 'fits the generated image/pdf into the specified width, preserves aspect ratio.', parseInt) 477 | .option('--height ', 478 | 'fits the generated image/pdf into the specified height, preserves aspect ratio.', parseInt) 479 | .option('--crop', 480 | 'crops PDF to diagram size') 481 | .option('-a, --all-pages', 482 | 'export all pages (for PDF format only)') 483 | .option('-p, --page-index ', 484 | 'selects a specific page (1-based); if not specified and the format is an image, the first page is selected', (i) => parseInt(i) - 1) 485 | .option('-l, --layers ', 486 | 'selects which layers to export (applies to all pages), if not specified, all layers are selected') 487 | .option('-g, --page-range ..', 488 | 'selects a page range (1-based, for PDF format only)', argsRange) 489 | .option('-u, --uncompressed', 490 | 'Uncompressed XML output (for XML format only)') 491 | .option('-z, --zoom ', 492 | 'scales the application interface', parseFloat) 493 | .option('--svg-theme ', 494 | 'Theme of the exported SVG image (dark, light, auto [default])', themeRegExp, 'auto') 495 | .option('--svg-links-target ', 496 | 'Target of links in the exported SVG image (auto [default], new-win, same-win)', linkTargetRegExp, 'auto') 497 | .option('--enable-plugins', 498 | 'Enable Plugins') 499 | .parse(argv) 500 | } 501 | catch(e) 502 | { 503 | //On parse error, return [exit and commander will show the error message] 504 | return; 505 | } 506 | 507 | var options = program.opts(); 508 | enablePlugins = options.enablePlugins; 509 | 510 | if (options.zoom != null) 511 | { 512 | appZoom = options.zoom; 513 | } 514 | 515 | //Start export mode? 516 | if (options.export) 517 | { 518 | var dummyWin = new BrowserWindow({ 519 | show : false, 520 | webPreferences: { 521 | preload: `${__dirname}/electron-preload.js`, 522 | contextIsolation: true, 523 | disableBlinkFeatures: 'Auxclick' // Is this needed? 524 | } 525 | }); 526 | 527 | windowsRegistry.push(dummyWin); 528 | 529 | /*ipcMain.on('log', function(event, msg) 530 | { 531 | console.log(msg); 532 | });*/ 533 | 534 | try 535 | { 536 | //Prepare arguments and confirm it's valid 537 | var format = null; 538 | var outType = null; 539 | 540 | //Format & Output 541 | if (options.output) 542 | { 543 | try 544 | { 545 | var outStat = fs.statSync(options.output); 546 | 547 | if (outStat.isDirectory()) 548 | { 549 | outType = {isDir: true}; 550 | } 551 | else //If we can get file stat, then it exists 552 | { 553 | throw 'Error: Output file already exists'; 554 | } 555 | } 556 | catch(e) //on error, file doesn't exist and it is not a dir 557 | { 558 | outType = {isFile: true}; 559 | 560 | format = path.extname(options.output).substr(1); 561 | 562 | if (!validFormatRegExp.test(format)) 563 | { 564 | format = null; 565 | } 566 | } 567 | } 568 | 569 | if (format == null) 570 | { 571 | format = options.format; 572 | } 573 | 574 | let from = null, to = null; 575 | 576 | if (options.pageIndex != null && options.pageIndex >= 0) 577 | { 578 | from = options.pageIndex; 579 | to = options.pageIndex; 580 | options.allPages = false; 581 | } 582 | else if (options.pageRange && options.pageRange.length == 2) 583 | { 584 | const [rangeFrom, rangeTo] = options.pageRange; 585 | 586 | if (rangeFrom >= 0 && rangeTo >= 0 && rangeFrom <= rangeTo) 587 | { 588 | from = rangeFrom; 589 | to = rangeTo; 590 | options.allPages = false; 591 | } 592 | else 593 | { 594 | console.error('Invalid page range: must be non-negative and from ≤ to'); 595 | process.exit(1); 596 | } 597 | } 598 | 599 | var expArgs = { 600 | format: format, 601 | w: options.width > 0 ? options.width : null, 602 | h: options.height > 0 ? options.height : null, 603 | bg: options.transparent ? 'none' : '#ffffff', 604 | from: from, 605 | to: to, 606 | allPages: format == 'pdf' && options.allPages, 607 | scale: (options.scale || 1), 608 | embedXml: options.embedDiagram? '1' : '0', 609 | embedImages: options.embedSvgImages? '1' : '0', 610 | embedFonts: options.embedSvgFonts? '1' : '0', 611 | jpegQuality: options.quality, 612 | uncompressed: options.uncompressed, 613 | theme: options.svgTheme, 614 | linkTarget: options.svgLinksTarget, 615 | crop: (options.crop && format == 'pdf') ? '1' : '0' 616 | }; 617 | 618 | options.border = options.border > 0 ? options.border : 0; 619 | 620 | if (format === 'pdf') 621 | { 622 | expArgs.pageMargin = options.border; 623 | } 624 | else 625 | { 626 | expArgs.border = options.border; 627 | } 628 | 629 | if (options.layers) 630 | { 631 | expArgs.extras = JSON.stringify({layers: options.layers.split(',')}); 632 | } 633 | 634 | var paths = program.args; 635 | 636 | // Remove --no-sandbox arg from the paths 637 | if (Array.isArray(paths)) 638 | { 639 | paths = paths.filter(function(path) { return path != null && path != '--no-sandbox'; }); 640 | } 641 | 642 | // If a file is passed 643 | if (paths !== undefined && paths[0] != null) 644 | { 645 | var inStat = null; 646 | 647 | try 648 | { 649 | inStat = fs.statSync(paths[0]); 650 | } 651 | catch(e) 652 | { 653 | throw 'Error: input file/directory not found'; 654 | } 655 | 656 | var files = []; 657 | 658 | function addDirectoryFiles(dir, isRecursive) 659 | { 660 | fs.readdirSync(dir).forEach(function(file) 661 | { 662 | var filePath = path.join(dir, file); 663 | var stat = fs.statSync(filePath); 664 | 665 | if (stat.isFile() && path.basename(filePath).charAt(0) != '.') 666 | { 667 | files.push(filePath); 668 | } 669 | if (stat.isDirectory() && isRecursive) 670 | { 671 | addDirectoryFiles(filePath, isRecursive) 672 | } 673 | }); 674 | } 675 | 676 | if (inStat.isFile()) 677 | { 678 | files.push(paths[0]); 679 | } 680 | else if (inStat.isDirectory()) 681 | { 682 | addDirectoryFiles(paths[0], options.recursive); 683 | } 684 | 685 | if (files.length > 0) 686 | { 687 | var fileIndex = 0; 688 | 689 | function processOneFile() 690 | { 691 | var curFile = files[fileIndex]; 692 | 693 | try 694 | { 695 | var ext = path.extname(curFile); 696 | 697 | let fileContent = fs.readFileSync(curFile, ext === '.png' || ext === '.vsdx' ? null : 'utf-8'); 698 | 699 | if (ext === '.vsdx') 700 | { 701 | dummyWin.loadURL(`file://${codeDir}/vsdxImporter.html`); 702 | 703 | const contents = dummyWin.webContents; 704 | 705 | contents.on('did-finish-load', function() 706 | { 707 | contents.send('import', fileContent); 708 | 709 | ipcMain.once('import-success', function(e, xml) 710 | { 711 | if (!validateSender(e.senderFrame)) return null; 712 | 713 | expArgs.xml = xml; 714 | startExport(); 715 | }); 716 | 717 | ipcMain.once('import-error', function(e) 718 | { 719 | if (!validateSender(e.senderFrame)) return null; 720 | 721 | console.error('Error: cannot import VSDX file: ' + curFile); 722 | next(); 723 | }); 724 | }); 725 | } 726 | else 727 | { 728 | if (ext === '.csv') 729 | { 730 | expArgs.csv = fileContent; 731 | } 732 | else if (ext === '.png') 733 | { 734 | expArgs.xmlEncoded = true; 735 | expArgs.xml = Buffer.from(fileContent).toString('base64'); 736 | } 737 | else 738 | { 739 | expArgs.xml = fileContent; 740 | } 741 | 742 | startExport(); 743 | } 744 | 745 | function next() 746 | { 747 | fileIndex++; 748 | 749 | if (fileIndex < files.length) 750 | { 751 | processOneFile(); 752 | } 753 | else 754 | { 755 | cmdQPressed = true; 756 | dummyWin.destroy(); 757 | } 758 | }; 759 | 760 | function startExport() 761 | { 762 | var mockEvent = { 763 | reply: function(msg, data) 764 | { 765 | try 766 | { 767 | if (data == null || data.length == 0) 768 | { 769 | console.error('Error: Export failed: ' + curFile); 770 | } 771 | else if (msg == 'export-success') 772 | { 773 | var outFileName = null; 774 | 775 | if (outType != null) 776 | { 777 | if (outType.isDir) 778 | { 779 | outFileName = path.join(options.output, path.basename(curFile, 780 | path.extname(curFile))) + '.' + format; 781 | } 782 | else 783 | { 784 | outFileName = options.output; 785 | } 786 | } 787 | else if (inStat.isFile()) 788 | { 789 | outFileName = path.join(path.dirname(paths[0]), path.basename(paths[0], 790 | path.extname(paths[0]))) + '.' + format; 791 | 792 | } 793 | else //dir 794 | { 795 | outFileName = path.join(path.dirname(curFile), path.basename(curFile, 796 | path.extname(curFile))) + '.' + format; 797 | } 798 | 799 | try 800 | { 801 | var counter = 0; 802 | var realFileName = outFileName; 803 | 804 | if (program.rawArgs.indexOf('-k') > -1 || program.rawArgs.indexOf('--check') > -1) 805 | { 806 | while (fs.existsSync(realFileName)) 807 | { 808 | counter++; 809 | realFileName = path.join(path.dirname(outFileName), path.basename(outFileName, 810 | path.extname(outFileName))) + '-' + counter + path.extname(outFileName); 811 | } 812 | } 813 | 814 | fs.writeFileSync(realFileName, data, null, { flag: 'wx' }); 815 | console.log(curFile + ' -> ' + realFileName); 816 | } 817 | catch(e) 818 | { 819 | console.error('Error writing to file: ' + outFileName); 820 | } 821 | } 822 | else 823 | { 824 | console.error('Error: ' + data + ': ' + curFile); 825 | } 826 | 827 | next(); 828 | } 829 | finally 830 | { 831 | mockEvent.finalize(); 832 | } 833 | } 834 | }; 835 | 836 | exportDiagram(mockEvent, expArgs, true); 837 | }; 838 | } 839 | catch(e) 840 | { 841 | console.error('Error reading file: ' + curFile); 842 | next(); 843 | } 844 | } 845 | 846 | processOneFile(); 847 | } 848 | else 849 | { 850 | throw 'Error: input file/directory not found or directory is empty'; 851 | } 852 | } 853 | else 854 | { 855 | throw 'Error: An input file must be specified'; 856 | } 857 | } 858 | catch(e) 859 | { 860 | console.error(e); 861 | 862 | cmdQPressed = true; 863 | dummyWin.destroy(); 864 | } 865 | 866 | return; 867 | } 868 | else if (program.rawArgs.indexOf('-h') > -1 || program.rawArgs.indexOf('--help') > -1 || program.rawArgs.indexOf('-V') > -1 || program.rawArgs.indexOf('--version') > -1) //To prevent execution when help/version arg is used 869 | { 870 | app.quit(); 871 | return; 872 | } 873 | 874 | //Prevent multiple instances of the application (casuses issues with configuration) 875 | const gotTheLock = app.requestSingleInstanceLock() 876 | 877 | if (!gotTheLock) 878 | { 879 | app.quit() 880 | } 881 | else 882 | { 883 | app.on('second-instance', (event, commandLine, workingDirectory) => { 884 | // Creating a new window while a save/open dialog is open crashes the app 885 | if (dialogOpen) return; 886 | 887 | //Create another window 888 | let win = createWindow() 889 | 890 | let loadEvtCount = 0; 891 | 892 | function loadFinished(e) 893 | { 894 | if (e != null && !validateSender(e.senderFrame)) return null; 895 | 896 | loadEvtCount++; 897 | 898 | if (loadEvtCount == 2) 899 | { 900 | //Open the file if new app request is from opening a file 901 | var potFile = commandLine.pop(); 902 | 903 | if (fs.existsSync(potFile)) 904 | { 905 | win.webContents.send('args-obj', {args: [potFile]}); 906 | } 907 | } 908 | } 909 | 910 | //Order of these two events is not guaranteed, so wait for them async. 911 | //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly 912 | ipcMain.once('app-load-finished', loadFinished); 913 | 914 | win.webContents.on('did-finish-load', function() 915 | { 916 | win.webContents.zoomFactor = appZoom; 917 | win.webContents.setVisualZoomLevelLimits(1, appZoom); 918 | loadFinished(); 919 | }); 920 | }) 921 | } 922 | 923 | let win = createWindow() 924 | 925 | let loadEvtCount = 0; 926 | 927 | function loadFinished(e) 928 | { 929 | if (e != null && !validateSender(e.senderFrame)) return null; 930 | 931 | loadEvtCount++; 932 | 933 | if (loadEvtCount == 2) 934 | { 935 | //Sending entire program is not allowed in Electron 9 as it is not native JS object 936 | win.webContents.send('args-obj', {args: program.args, create: options.create}); 937 | } 938 | } 939 | 940 | //Order of these two events is not guaranteed, so wait for them async. 941 | //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly 942 | ipcMain.once('app-load-finished', loadFinished); 943 | 944 | win.webContents.on('did-finish-load', function() 945 | { 946 | if (firstWinFilePath != null) 947 | { 948 | if (program.args != null) 949 | { 950 | program.args.push(firstWinFilePath); 951 | } 952 | else 953 | { 954 | program.args = [firstWinFilePath]; 955 | } 956 | } 957 | 958 | firstWinLoaded = true; 959 | 960 | win.webContents.zoomFactor = appZoom; 961 | win.webContents.setVisualZoomLevelLimits(1, appZoom); 962 | loadFinished(); 963 | }); 964 | 965 | function toggleSpellCheck(e) 966 | { 967 | if (e != null && !validateSender(e.senderFrame)) return null; 968 | 969 | if (store != null) 970 | { 971 | enableSpellCheck = !enableSpellCheck; 972 | store.set('enableSpellCheck', enableSpellCheck); 973 | } 974 | }; 975 | 976 | ipcMain.on('toggleSpellCheck', toggleSpellCheck); 977 | 978 | function toggleStoreBkp(e) 979 | { 980 | if (e != null && !validateSender(e.senderFrame)) return null; 981 | 982 | if (store != null) 983 | { 984 | enableStoreBkp = !enableStoreBkp; 985 | store.set('enableStoreBkp', enableStoreBkp); 986 | } 987 | }; 988 | 989 | ipcMain.on('toggleStoreBkp', toggleStoreBkp); 990 | 991 | function toggleGoogleFonts(e) 992 | { 993 | if (e != null && !validateSender(e.senderFrame)) return null; 994 | 995 | if (store != null) 996 | { 997 | isGoogleFontsEnabled = !isGoogleFontsEnabled; 998 | store.set('isGoogleFontsEnabled', isGoogleFontsEnabled); 999 | } 1000 | } 1001 | 1002 | ipcMain.on('toggleGoogleFonts', toggleGoogleFonts); 1003 | 1004 | function toggleFullscreen(e) 1005 | { 1006 | if (e != null && !validateSender(e.senderFrame)) return null; 1007 | 1008 | let win = BrowserWindow.getFocusedWindow(); 1009 | 1010 | if (win != null) 1011 | { 1012 | win.setFullScreen(!win.isFullScreen()); 1013 | } 1014 | }; 1015 | 1016 | ipcMain.on('toggleFullscreen', toggleFullscreen); 1017 | 1018 | let updateNoAvailAdded = false; 1019 | 1020 | function checkForUpdatesFn(e) 1021 | { 1022 | if (e != null && e.senderFrame != null && 1023 | !validateSender(e.senderFrame)) return null; 1024 | 1025 | autoUpdater.checkForUpdates(); 1026 | 1027 | if (store != null) 1028 | { 1029 | store.set('dontCheckUpdates', false); 1030 | } 1031 | 1032 | if (!updateNoAvailAdded) 1033 | { 1034 | updateNoAvailAdded = true; 1035 | autoUpdater.on('update-not-available', (info) => { 1036 | dialog.showMessageBox( 1037 | { 1038 | type: 'info', 1039 | title: 'No updates found', 1040 | message: 'Your application is up-to-date', 1041 | }) 1042 | }) 1043 | } 1044 | }; 1045 | 1046 | var zoomSteps = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1047 | 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]; 1048 | 1049 | // Zooms to the next zoom step 1050 | function zoomInFn() 1051 | { 1052 | var zoomFactor = win.webContents.zoomFactor; 1053 | var newZoomFactor = zoomSteps[zoomSteps.length - 1]; 1054 | 1055 | for (var i = 0; i < zoomSteps.length; i++) 1056 | { 1057 | if (zoomSteps[i] - zoomFactor > 0.01) 1058 | { 1059 | newZoomFactor = zoomSteps[i]; 1060 | break; 1061 | } 1062 | } 1063 | 1064 | win.webContents.zoomFactor = newZoomFactor; 1065 | }; 1066 | 1067 | // Zooms to the previous zoom step 1068 | function zoomOutFn() 1069 | { 1070 | var zoomFactor = win.webContents.zoomFactor; 1071 | var newZoomFactor = zoomSteps[0]; 1072 | 1073 | for (var i = zoomSteps.length - 1; i >= 0; i--) 1074 | { 1075 | if (zoomSteps[i] - zoomFactor < -0.01) 1076 | { 1077 | newZoomFactor = zoomSteps[i]; 1078 | break; 1079 | } 1080 | } 1081 | 1082 | win.webContents.zoomFactor = newZoomFactor; 1083 | }; 1084 | 1085 | // Resets the zoom factor 1086 | function resetZoomFn() 1087 | { 1088 | win.webContents.zoomFactor = 1; 1089 | }; 1090 | 1091 | let checkForUpdates = { 1092 | label: 'Check for updates', 1093 | click: checkForUpdatesFn 1094 | } 1095 | 1096 | let zoomIn = { 1097 | label: 'Zoom In', 1098 | click: zoomInFn 1099 | }; 1100 | 1101 | let zoomOut = { 1102 | label: 'Zoom Out', 1103 | click: zoomOutFn 1104 | }; 1105 | 1106 | let resetZoom = { 1107 | label: 'Actual Size', 1108 | click: resetZoomFn 1109 | }; 1110 | 1111 | ipcMain.on('checkForUpdates', checkForUpdatesFn); 1112 | ipcMain.on('zoomIn', zoomInFn); 1113 | ipcMain.on('zoomOut', zoomOutFn); 1114 | ipcMain.on('resetZoom', resetZoomFn); 1115 | 1116 | if (isMac) 1117 | { 1118 | let template = [{ 1119 | label: app.name, 1120 | submenu: [ 1121 | { 1122 | label: 'About ' + app.name, 1123 | click() { shell.openExternal('https://www.drawio.com'); } 1124 | }, 1125 | { 1126 | label: 'Support', 1127 | click() { shell.openExternal('https://github.com/jgraph/drawio-desktop/issues'); } 1128 | }, 1129 | checkForUpdates, 1130 | { type: 'separator' }, 1131 | resetZoom, 1132 | zoomIn, 1133 | zoomOut, 1134 | { type: 'separator' }, 1135 | { role: 'hide' }, 1136 | { role: 'hideothers' }, 1137 | { role: 'unhide' }, 1138 | { type: 'separator' }, 1139 | { role: 'quit' } 1140 | ] 1141 | }, { 1142 | label: 'Edit', 1143 | submenu: [ 1144 | { role: 'undo' }, 1145 | { role: 'redo' }, 1146 | { type: 'separator' }, 1147 | { role: 'cut' }, 1148 | { role: 'copy' }, 1149 | { role: 'paste' }, 1150 | { role: 'pasteAndMatchStyle' }, 1151 | { role: 'selectAll' } 1152 | ] 1153 | }] 1154 | 1155 | if (disableUpdate) 1156 | { 1157 | template[0].submenu.splice(2, 1); 1158 | } 1159 | 1160 | const menuBar = menu.buildFromTemplate(template) 1161 | menu.setApplicationMenu(menuBar) 1162 | } 1163 | else //hide menubar in win/linux 1164 | { 1165 | menu.setApplicationMenu(null) 1166 | } 1167 | 1168 | autoUpdater.setFeedURL({ 1169 | provider: 'github', 1170 | repo: 'drawio-desktop', 1171 | owner: 'jgraph' 1172 | }) 1173 | 1174 | if (store == null || (!disableUpdate && !store.get('dontCheckUpdates'))) 1175 | { 1176 | autoUpdater.checkForUpdates() 1177 | } 1178 | }) 1179 | 1180 | //Quit from the dock context menu should quit the application directly 1181 | if (isMac) 1182 | { 1183 | app.on('before-quit', function() { 1184 | cmdQPressed = true; 1185 | }); 1186 | } 1187 | 1188 | // Quit when all windows are closed. 1189 | app.on('window-all-closed', function () 1190 | { 1191 | if (__DEV__) 1192 | { 1193 | console.log('window-all-closed', windowsRegistry.length) 1194 | } 1195 | 1196 | // On OS X it is common for applications and their menu bar 1197 | // to stay active until the user quits explicitly with Cmd + Q 1198 | if (cmdQPressed || !isMac) 1199 | { 1200 | app.quit() 1201 | } 1202 | }) 1203 | 1204 | app.on('activate', function () 1205 | { 1206 | if (__DEV__) 1207 | { 1208 | console.log('app on activate', windowsRegistry.length) 1209 | } 1210 | 1211 | // On OS X it's common to re-create a window in the app when the 1212 | // dock icon is clicked and there are no other windows open. 1213 | if (windowsRegistry.length === 0) 1214 | { 1215 | createWindow() 1216 | } 1217 | }) 1218 | 1219 | app.on('will-finish-launching', function() 1220 | { 1221 | app.on("open-file", function(event, filePath) 1222 | { 1223 | event.preventDefault(); 1224 | // Creating a new window while a save/open dialog is open crashes the app 1225 | if (dialogOpen) return; 1226 | 1227 | if (firstWinLoaded) 1228 | { 1229 | let win = createWindow(); 1230 | 1231 | let loadEvtCount = 0; 1232 | 1233 | function loadFinished(e) 1234 | { 1235 | if (e != null && !validateSender(e.senderFrame)) return null; 1236 | 1237 | loadEvtCount++; 1238 | 1239 | if (loadEvtCount == 2) 1240 | { 1241 | win.webContents.send('args-obj', {args: [filePath]}); 1242 | } 1243 | } 1244 | 1245 | //Order of these two events is not guaranteed, so wait for them async. 1246 | //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly 1247 | ipcMain.once('app-load-finished', loadFinished); 1248 | 1249 | win.webContents.on('did-finish-load', function() 1250 | { 1251 | win.webContents.zoomFactor = appZoom; 1252 | win.webContents.setVisualZoomLevelLimits(1, appZoom); 1253 | loadFinished(); 1254 | }); 1255 | } 1256 | else 1257 | { 1258 | firstWinFilePath = filePath 1259 | } 1260 | }); 1261 | }); 1262 | 1263 | app.on('web-contents-created', (event, contents) => { 1264 | // Disable navigation 1265 | contents.on('will-navigate', (event, navigationUrl) => { 1266 | event.preventDefault() 1267 | }) 1268 | 1269 | // Limit creation of new windows (we also override window.open) 1270 | contents.setWindowOpenHandler(({ url }) => { 1271 | // We allow external absolute URLs to be open externally (check openExternal for details) and also empty windows (url -> about:blank) 1272 | if (url.startsWith('about:blank')) 1273 | { 1274 | return { 1275 | action: 'allow', 1276 | overrideBrowserWindowOptions: { 1277 | fullscreenable: false, 1278 | webPreferences: { 1279 | contextIsolation: true 1280 | } 1281 | } 1282 | } 1283 | } 1284 | else if (!openExternal(url)) 1285 | { 1286 | return {action: 'deny'} 1287 | } 1288 | }) 1289 | 1290 | // Disable all webviews 1291 | contents.on('will-attach-webview', (event, webPreferences, params) => { 1292 | event.preventDefault() 1293 | }) 1294 | }) 1295 | 1296 | autoUpdater.on('error', e => log.error('@error@\n', e)) 1297 | 1298 | autoUpdater.on('update-available', (a, b) => 1299 | { 1300 | if (silentUpdate) return; 1301 | 1302 | dialog.showMessageBox( 1303 | { 1304 | type: 'question', 1305 | buttons: ['Ok', 'Cancel', 'Don\'t Ask Again'], 1306 | title: 'Confirm Update', 1307 | message: 'Update available.\n\nWould you like to download and install new version?', 1308 | detail: 'Application will automatically restart to apply update after download', 1309 | }).then( result => 1310 | { 1311 | if (result.response === 0) 1312 | { 1313 | autoUpdater.downloadUpdate() 1314 | 1315 | var progressBar = new ProgressBar({ 1316 | title: 'draw.io Update', 1317 | text: 'Downloading draw.io update...' 1318 | }); 1319 | 1320 | function reportUpdateError(e) 1321 | { 1322 | progressBar.detail = 'Error occurred while fetching updates. ' + (e && e.message? e.message : e) 1323 | progressBar._window.setClosable(true); 1324 | } 1325 | 1326 | autoUpdater.on('error', e => { 1327 | if (progressBar._window != null) 1328 | { 1329 | reportUpdateError(e); 1330 | } 1331 | else 1332 | { 1333 | progressBar.on('ready', function() { 1334 | reportUpdateError(e); 1335 | }); 1336 | } 1337 | }) 1338 | 1339 | var firstTimeProg = true; 1340 | 1341 | autoUpdater.on('download-progress', (d) => { 1342 | //On mac, download-progress event is not called, so the indeterminate progress will continue until download is finished 1343 | var percent = d.percent; 1344 | 1345 | if (percent) 1346 | { 1347 | percent = Math.round(percent * 100)/100; 1348 | } 1349 | 1350 | if (firstTimeProg) 1351 | { 1352 | firstTimeProg = false; 1353 | progressBar.close(); 1354 | 1355 | progressBar = new ProgressBar({ 1356 | indeterminate: false, 1357 | title: 'draw.io Update', 1358 | text: 'Downloading draw.io update...', 1359 | detail: `${percent}% ...`, 1360 | initialValue: percent 1361 | }); 1362 | 1363 | progressBar 1364 | .on('completed', function() { 1365 | progressBar.detail = 'Download completed.'; 1366 | }) 1367 | .on('aborted', function(value) { 1368 | if (__DEV__) 1369 | { 1370 | log.error(`progress aborted... ${value}`); 1371 | } 1372 | }) 1373 | .on('progress', function(value) { 1374 | progressBar.detail = `${value}% ...`; 1375 | }) 1376 | .on('ready', function() { 1377 | //InitialValue doesn't set the UI! so this is needed to render it correctly 1378 | progressBar.value = percent; 1379 | }); 1380 | } 1381 | else 1382 | { 1383 | progressBar.value = percent; 1384 | } 1385 | }); 1386 | 1387 | autoUpdater.on('update-downloaded', (info) => { 1388 | if (!progressBar.isCompleted()) 1389 | { 1390 | progressBar.close() 1391 | } 1392 | 1393 | // Ask user to update the app 1394 | dialog.showMessageBox( 1395 | { 1396 | type: 'question', 1397 | buttons: ['Install', 'Later'], 1398 | defaultId: 0, 1399 | message: 'A new version of ' + app.name + ' has been downloaded', 1400 | detail: 'It will be installed the next time you restart the application', 1401 | }).then(result => 1402 | { 1403 | if (result.response === 0) 1404 | { 1405 | setTimeout(() => autoUpdater.quitAndInstall(), 1) 1406 | } 1407 | }) 1408 | }); 1409 | } 1410 | else if (result.response === 2 && store != null) 1411 | { 1412 | //save in settings don't check for updates 1413 | store.set('dontCheckUpdates', true) 1414 | } 1415 | }) 1416 | }) 1417 | 1418 | //Pdf export 1419 | const MICRON_TO_PIXEL = 264.58 //264.58 micron = 1 pixel 1420 | const PIXELS_PER_INCH = 100.117 // Usually it is 100 pixels per inch but this give better results 1421 | const PNG_CHUNK_IDAT = 1229209940; 1422 | const LARGE_IMAGE_AREA = 30000000; 1423 | 1424 | //NOTE: Key length must not be longer than 79 bytes (not checked) 1425 | function writePngWithText(origBuff, key, text, compressed, base64encoded) 1426 | { 1427 | var isDpi = key == 'dpi'; 1428 | var inOffset = 0; 1429 | var outOffset = 0; 1430 | var data = text; 1431 | var dataLen = isDpi? 9 : key.length + data.length + 1; //we add 1 zeros with non-compressed data, for pHYs it's 2 of 4-byte-int + 1 byte 1432 | 1433 | //prepare compressed data to get its size 1434 | if (compressed) 1435 | { 1436 | data = zlib.deflateRawSync(encodeURIComponent(text)); 1437 | dataLen = key.length + data.length + 2; //we add 2 zeros with compressed data 1438 | } 1439 | 1440 | var outBuff = Buffer.allocUnsafe(origBuff.length + dataLen + 4); //4 is the header size "zTXt", "tEXt" or "pHYs" 1441 | 1442 | try 1443 | { 1444 | var magic1 = origBuff.readUInt32BE(inOffset); 1445 | inOffset += 4; 1446 | var magic2 = origBuff.readUInt32BE(inOffset); 1447 | inOffset += 4; 1448 | 1449 | if (magic1 != 0x89504e47 && magic2 != 0x0d0a1a0a) 1450 | { 1451 | throw new Error("PNGImageDecoder0"); 1452 | } 1453 | 1454 | outBuff.writeUInt32BE(magic1, outOffset); 1455 | outOffset += 4; 1456 | outBuff.writeUInt32BE(magic2, outOffset); 1457 | outOffset += 4; 1458 | } 1459 | catch (e) 1460 | { 1461 | log.error(e.message, {stack: e.stack}); 1462 | throw new Error("PNGImageDecoder1"); 1463 | } 1464 | 1465 | try 1466 | { 1467 | while (inOffset < origBuff.length) 1468 | { 1469 | var length = origBuff.readInt32BE(inOffset); 1470 | inOffset += 4; 1471 | var type = origBuff.readInt32BE(inOffset) 1472 | inOffset += 4; 1473 | 1474 | if (type == PNG_CHUNK_IDAT) 1475 | { 1476 | // Insert zTXt chunk before IDAT chunk 1477 | outBuff.writeInt32BE(dataLen, outOffset); 1478 | outOffset += 4; 1479 | 1480 | var typeSignature = isDpi? 'pHYs' : (compressed ? "zTXt" : "tEXt"); 1481 | outBuff.write(typeSignature, outOffset); 1482 | 1483 | outOffset += 4; 1484 | 1485 | if (isDpi) 1486 | { 1487 | var dpm = Math.round(parseInt(text) / 0.0254) || 3937; //One inch is equal to exactly 0.0254 meters. 3937 is 100dpi 1488 | 1489 | outBuff.writeInt32BE(dpm, outOffset); 1490 | outBuff.writeInt32BE(dpm, outOffset + 4); 1491 | outBuff.writeInt8(1, outOffset + 8); 1492 | outOffset += 9; 1493 | 1494 | data = Buffer.allocUnsafe(9); 1495 | data.writeInt32BE(dpm, 0); 1496 | data.writeInt32BE(dpm, 4); 1497 | data.writeInt8(1, 8); 1498 | } 1499 | else 1500 | { 1501 | outBuff.write(key, outOffset); 1502 | outOffset += key.length; 1503 | outBuff.writeInt8(0, outOffset); 1504 | outOffset ++; 1505 | 1506 | if (compressed) 1507 | { 1508 | outBuff.writeInt8(0, outOffset); 1509 | outOffset ++; 1510 | data.copy(outBuff, outOffset); 1511 | } 1512 | else 1513 | { 1514 | outBuff.write(data, outOffset); 1515 | } 1516 | 1517 | outOffset += data.length; 1518 | } 1519 | 1520 | var crcVal = 0xffffffff; 1521 | crcVal = crc.crcjam(typeSignature, crcVal); 1522 | crcVal = crc.crcjam(data, crcVal); 1523 | 1524 | // CRC 1525 | outBuff.writeInt32BE(crcVal ^ 0xffffffff, outOffset); 1526 | outOffset += 4; 1527 | 1528 | // Writes the IDAT chunk after the zTXt 1529 | outBuff.writeInt32BE(length, outOffset); 1530 | outOffset += 4; 1531 | outBuff.writeInt32BE(type, outOffset); 1532 | outOffset += 4; 1533 | 1534 | origBuff.copy(outBuff, outOffset, inOffset); 1535 | 1536 | // Encodes the buffer using base64 if requested 1537 | return base64encoded? outBuff.toString('base64') : outBuff; 1538 | } 1539 | 1540 | outBuff.writeInt32BE(length, outOffset); 1541 | outOffset += 4; 1542 | outBuff.writeInt32BE(type, outOffset); 1543 | outOffset += 4; 1544 | 1545 | origBuff.copy(outBuff, outOffset, inOffset, inOffset + length + 4);// +4 to move past the crc 1546 | 1547 | inOffset += length + 4; 1548 | outOffset += length + 4; 1549 | } 1550 | } 1551 | catch (e) 1552 | { 1553 | log.error(e.message, {stack: e.stack}); 1554 | throw e; 1555 | } 1556 | } 1557 | 1558 | async function mergePdfs(pdfFiles, xml) 1559 | { 1560 | if (pdfFiles.length == 1) 1561 | { 1562 | // Converts to PDF 1.7 with compression 1563 | const pdfDoc = await PDFDocument.load(pdfFiles[0]); 1564 | pdfDoc.setCreator('diagrams.net'); 1565 | 1566 | // KNOWN: Attachments produce smaller files but break 1567 | // internal links in pdf-lib so using Subject for now 1568 | if (xml != null) 1569 | { 1570 | pdfDoc.setSubject(encodeURIComponent(xml). 1571 | replace(/\(/g, "\\(").replace(/\)/g, "\\)")); 1572 | } 1573 | 1574 | const pdfBytes = await pdfDoc.save(); 1575 | 1576 | return Buffer.from(pdfBytes); 1577 | } 1578 | 1579 | try 1580 | { 1581 | const pdfDoc = await PDFDocument.create(); 1582 | pdfDoc.setCreator('diagrams.net'); 1583 | 1584 | if (xml != null) 1585 | { 1586 | //Embed diagram XML as file attachment 1587 | await pdfDoc.attach(Buffer.from(xml).toString('base64'), 'diagram.xml', { 1588 | mimeType: 'application/vnd.jgraph.mxfile', 1589 | description: 'Diagram Content' 1590 | }); 1591 | } 1592 | 1593 | for (var i = 0; i < pdfFiles.length; i++) 1594 | { 1595 | const pdfFile = await PDFDocument.load(pdfFiles[i].buffer); 1596 | const pages = await pdfDoc.copyPages(pdfFile, pdfFile.getPageIndices()); 1597 | pages.forEach(p => pdfDoc.addPage(p)); 1598 | } 1599 | 1600 | const pdfBytes = await pdfDoc.save(); 1601 | return Buffer.from(pdfBytes); 1602 | } 1603 | catch(e) 1604 | { 1605 | throw new Error('Error during PDF combination: ' + e.message); 1606 | } 1607 | } 1608 | 1609 | //TODO Use canvas to export images if math is not used to speedup export (no capturePage). Requires change to export3.html also 1610 | function exportDiagram(event, args, directFinalize) 1611 | { 1612 | if (event != null && event.senderFrame != null && 1613 | !validateSender(event.senderFrame)) return null; 1614 | 1615 | var browser = null; 1616 | 1617 | try 1618 | { 1619 | browser = new BrowserWindow({ 1620 | webPreferences: { 1621 | preload: `${__dirname}/electron-preload.js`, 1622 | backgroundThrottling: false, 1623 | contextIsolation: true, 1624 | disableBlinkFeatures: 'Auxclick', // Is this needed? 1625 | offscreen: true, 1626 | }, 1627 | show : false, 1628 | frame: false, 1629 | enableLargerThanScreen: true, 1630 | transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'), 1631 | parent: windowsRegistry[0] //set parent to first opened window. Not very accurate, but useful when all visible windows are closed 1632 | }); 1633 | 1634 | browser.loadURL(`file://${codeDir}/export3.html`); 1635 | 1636 | const contents = browser.webContents; 1637 | var from = args.from; 1638 | var to = args.to; 1639 | var pdfs = []; 1640 | 1641 | contents.on('did-finish-load', function() 1642 | { 1643 | //Set finalize here since it is call in the reply below 1644 | function finalize() 1645 | { 1646 | browser.destroy(); 1647 | }; 1648 | 1649 | if (directFinalize === true) 1650 | { 1651 | event.finalize = finalize; 1652 | } 1653 | else 1654 | { 1655 | //Destroy the window after response being received by caller 1656 | ipcMain.once('export-finalize', finalize); 1657 | } 1658 | 1659 | function renderingFinishHandler(e, renderInfo) 1660 | { 1661 | if (!validateSender(e.senderFrame)) return null; 1662 | 1663 | if (renderInfo == null) 1664 | { 1665 | event.reply('export-error'); 1666 | return; 1667 | } 1668 | 1669 | var pageCount = renderInfo.pageCount, bounds = null; 1670 | //For some reason, Electron 9 doesn't send this object as is without stringifying. Usually when variable is external to function own scope 1671 | try 1672 | { 1673 | bounds = JSON.parse(renderInfo.bounds); 1674 | } 1675 | catch(e) 1676 | { 1677 | bounds = null; 1678 | } 1679 | 1680 | var pdfOptions = {}; 1681 | var hasError = false; 1682 | 1683 | if (bounds == null || bounds.width < 5 || bounds.height < 5) //very small page size never return from printToPDF 1684 | { 1685 | //A workaround to detect errors in the input file or being empty file 1686 | hasError = true; 1687 | } 1688 | else 1689 | { 1690 | pdfOptions = { 1691 | preferCSSPageSize: true, 1692 | printBackground: true 1693 | } 1694 | } 1695 | 1696 | var base64encoded = args.base64 == '1'; 1697 | 1698 | if (hasError) 1699 | { 1700 | event.reply('export-error'); 1701 | } 1702 | else if (args.format == 'png' || args.format == 'jpg' || args.format == 'jpeg') 1703 | { 1704 | //Adds an extra pixel to prevent scrollbars from showing 1705 | var newBounds = {width: Math.ceil(bounds.width + bounds.x) + 1, height: Math.ceil(bounds.height + bounds.y) + 1}; 1706 | browser.setBounds(newBounds); 1707 | 1708 | //TODO The browser takes sometime to show the graph (also after resize it takes some time to render) 1709 | // 1 sec is most probably enough (for small images, 5 for large ones) BUT not a stable solution 1710 | setTimeout(function() 1711 | { 1712 | browser.capturePage().then(function(img) 1713 | { 1714 | //Image is double the given bounds, so resize is needed! 1715 | var tScale = 1; 1716 | 1717 | //If user defined width and/or height, enforce it precisely here. Height override width 1718 | if (args.h) 1719 | { 1720 | tScale = args.h / newBounds.height; 1721 | } 1722 | else if (args.w) 1723 | { 1724 | tScale = args.w / newBounds.width; 1725 | } 1726 | 1727 | newBounds.width *= tScale; 1728 | newBounds.height *= tScale; 1729 | img = img.resize(newBounds); 1730 | 1731 | var data = args.format == 'png'? img.toPNG() : img.toJPEG(args.jpegQuality || 90); 1732 | 1733 | if (args.dpi != null && args.format == 'png') 1734 | { 1735 | data = writePngWithText(data, 'dpi', args.dpi); 1736 | } 1737 | 1738 | if (args.embedXml == "1" && args.format == 'png') 1739 | { 1740 | data = writePngWithText(data, "mxGraphModel", args.xml, true, 1741 | base64encoded); 1742 | } 1743 | else 1744 | { 1745 | if (base64encoded) 1746 | { 1747 | data = data.toString('base64'); 1748 | } 1749 | } 1750 | 1751 | event.reply('export-success', data); 1752 | }); 1753 | }, bounds.width * bounds.height < LARGE_IMAGE_AREA? 1000 : 5000); 1754 | } 1755 | else if (args.format == 'pdf') 1756 | { 1757 | if (args.print) 1758 | { 1759 | pdfOptions = { 1760 | scaleFactor: args.pageScale, 1761 | printBackground: true, 1762 | pageSize : { 1763 | width: args.pageWidth * MICRON_TO_PIXEL, 1764 | //This height adjustment fixes the output. TODO Test more cases 1765 | height: (args.pageHeight * 1.025) * MICRON_TO_PIXEL 1766 | }, 1767 | margins: { 1768 | marginType: 'none' // no margin 1769 | } 1770 | }; 1771 | 1772 | contents.print(pdfOptions, (success, errorType) => 1773 | { 1774 | //Consider all as success 1775 | event.reply('export-success', {}); 1776 | // Notify the user with an error if it fails 1777 | if (!success && errorType != 'Print job canceled') 1778 | { 1779 | dialog.showMessageBox(null, { 1780 | type: 'error', 1781 | title: 'Printing Error', 1782 | message: 'There was an error printing. ' + errorType 1783 | }); 1784 | } 1785 | }); 1786 | } 1787 | else 1788 | { 1789 | contents.printToPDF(pdfOptions).then(async (data) => 1790 | { 1791 | pdfs.push(data); 1792 | to = to > pageCount? pageCount : to; 1793 | from++; 1794 | 1795 | if (from < to) 1796 | { 1797 | args.from = from; 1798 | args.to = from; 1799 | ipcMain.once('render-finished', renderingFinishHandler); 1800 | contents.send('render', args); 1801 | } 1802 | else 1803 | { 1804 | // TODO extract the correct xml if the source was a pnd file 1805 | data = await mergePdfs(pdfs, args.embedXml == '1' ? args.xml : null); 1806 | event.reply('export-success', data); 1807 | } 1808 | }) 1809 | .catch((error) => 1810 | { 1811 | event.reply('export-error', error); 1812 | }); 1813 | } 1814 | } 1815 | else if (args.format == 'svg') 1816 | { 1817 | contents.send('get-svg-data'); 1818 | 1819 | ipcMain.once('svg-data', (e, data) => 1820 | { 1821 | if (!validateSender(e.senderFrame)) return null; 1822 | 1823 | event.reply('export-success', data); 1824 | }); 1825 | } 1826 | else 1827 | { 1828 | event.reply('export-error', 'Error: Unsupported format'); 1829 | } 1830 | }; 1831 | 1832 | ipcMain.once('render-finished', renderingFinishHandler); 1833 | 1834 | if (args.format == 'xml') 1835 | { 1836 | ipcMain.once('xml-data', (e, data) => 1837 | { 1838 | if (!validateSender(e.senderFrame)) return null; 1839 | 1840 | event.reply('export-success', data); 1841 | }); 1842 | 1843 | ipcMain.once('xml-data-error', (e) => 1844 | { 1845 | if (!validateSender(e.senderFrame)) return null; 1846 | 1847 | event.reply('export-error'); 1848 | }); 1849 | } 1850 | 1851 | args.border = args.border || 0; 1852 | args.scale = args.scale || 1; 1853 | 1854 | if (args.filename != null && args.filename != '') 1855 | { 1856 | var filename = decodeURIComponent(args.filename); 1857 | 1858 | if (filename.substring(filename.length - 4) == '.pdf') 1859 | { 1860 | filename = filename.substring(0, filename.length - 4); 1861 | } 1862 | 1863 | if (filename.substring(filename.length - 7) == '.drawio') 1864 | { 1865 | filename = filename.substring(0, filename.length - 7); 1866 | } 1867 | 1868 | args.fileTitle = filename; 1869 | } 1870 | 1871 | contents.send('render', args); 1872 | }); 1873 | } 1874 | catch (e) 1875 | { 1876 | if (browser != null) 1877 | { 1878 | browser.destroy(); 1879 | } 1880 | 1881 | event.reply('export-error', e); 1882 | console.log('export-error', e); 1883 | } 1884 | }; 1885 | 1886 | ipcMain.on('export', exportDiagram); 1887 | 1888 | //================================================================ 1889 | // Renderer Helper functions 1890 | //================================================================ 1891 | 1892 | const { O_SYNC, O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY } = fs.constants; 1893 | const DRAFT_PREFEX = '.$'; 1894 | const OLD_DRAFT_PREFEX = '~$'; 1895 | const DRAFT_EXT = '.dtmp'; 1896 | const BKP_PREFEX = '.$'; 1897 | const OLD_BKP_PREFEX = '~$'; 1898 | const BKP_EXT = '.bkp'; 1899 | 1900 | /** 1901 | * Checks the file content type 1902 | * Confirm content is xml, json, pdf, png, jpg, svg, vsdx ... 1903 | */ 1904 | function checkFileContent(body, enc) 1905 | { 1906 | if (body != null) 1907 | { 1908 | let head, headBinay; 1909 | 1910 | if (typeof body === 'string') 1911 | { 1912 | if (enc === 'base64') 1913 | { 1914 | headBinay = Buffer.from(body.substring(0, 22), 'base64'); 1915 | head = headBinay.toString(); 1916 | } 1917 | else 1918 | { 1919 | head = body.substring(0, 16); 1920 | headBinay = Buffer.from(head); 1921 | } 1922 | } 1923 | else 1924 | { 1925 | head = new TextDecoder("utf-8").decode(body.subarray(0, 16)); 1926 | headBinay = body; 1927 | } 1928 | 1929 | let c1 = head[0], 1930 | c2 = head[1], 1931 | c3 = head[2], 1932 | c4 = head[3], 1933 | c5 = head[4], 1934 | c6 = head[5], 1935 | c7 = head[6], 1936 | c8 = head[7], 1937 | c9 = head[8], 1938 | c10 = head[9], 1939 | c11 = head[10], 1940 | c12 = head[11], 1941 | c13 = head[12], 1942 | c14 = head[13], 1943 | c15 = head[14], 1944 | c16 = head[15]; 1945 | 1946 | let cc1 = headBinay[0], 1947 | cc2 = headBinay[1], 1948 | cc3 = headBinay[2], 1949 | cc4 = headBinay[3], 1950 | cc5 = headBinay[4], 1951 | cc6 = headBinay[5], 1952 | cc7 = headBinay[6], 1953 | cc8 = headBinay[7], 1954 | cc9 = headBinay[8], 1955 | cc10 = headBinay[9], 1956 | cc11 = headBinay[10], 1957 | cc12 = headBinay[11], 1958 | cc13 = headBinay[12], 1959 | cc14 = headBinay[13], 1960 | cc15 = headBinay[14], 1961 | cc16 = headBinay[15]; 1962 | 1963 | if (c1 == '<') 1964 | { 1965 | // text/html 1966 | if (c2 == '!' 1967 | || ((c2 == 'h' 1968 | && (c3 == 't' && c4 == 'm' && c5 == 'l' 1969 | || c3 == 'e' && c4 == 'a' && c5 == 'd') 1970 | || (c2 == 'b' && c3 == 'o' && c4 == 'd' 1971 | && c5 == 'y'))) 1972 | || ((c2 == 'H' 1973 | && (c3 == 'T' && c4 == 'M' && c5 == 'L' 1974 | || c3 == 'E' && c4 == 'A' && c5 == 'D') 1975 | || (c2 == 'B' && c3 == 'O' && c4 == 'D' 1976 | && c5 == 'Y')))) 1977 | { 1978 | return true; 1979 | } 1980 | 1981 | // application/xml 1982 | if (c2 == '?' && c3 == 'x' && c4 == 'm' && c5 == 'l' 1983 | && c6 == ' ') 1984 | { 1985 | return true; 1986 | } 1987 | 1988 | // application/svg+xml 1989 | if (c2 == 's' && c3 == 'v' && c4 == 'g' && c5 == ' ') 1990 | { 1991 | return true; 1992 | } 1993 | 1994 | // Embed cases img and iframe 1995 | if (c2 == 'i' && c3 == 'm' && c4 == 'g' && c5 == ' ' 1996 | || (c2 == 'i' && c3 == 'f' && c4 == 'r' && c5 == 'a' 1997 | && c6 == 'm' && c7 == 'e' && c8 == ' ')) 1998 | { 1999 | return true; 2000 | } 2001 | } 2002 | 2003 | // big and little (identical) endian UTF-8 encodings, with BOM 2004 | // application/xml 2005 | if (cc1 == 0xef && cc2 == 0xbb && cc3 == 0xbf) 2006 | { 2007 | if (c4 == '<' && c5 == '?' && c6 == 'x') 2008 | { 2009 | return true; 2010 | } 2011 | } 2012 | 2013 | // big and little endian UTF-16 encodings, with byte order mark 2014 | // application/xml 2015 | if (cc1 == 0xfe && cc2 == 0xff) 2016 | { 2017 | if (cc3 == 0 && c4 == '<' && cc5 == 0 && c6 == '?' && cc7 == 0 2018 | && c8 == 'x') 2019 | { 2020 | return true; 2021 | } 2022 | } 2023 | 2024 | // application/xml 2025 | if (cc1 == 0xff && cc2 == 0xfe) 2026 | { 2027 | if (c3 == '<' && cc4 == 0 && c5 == '?' && cc6 == 0 && c7 == 'x' 2028 | && cc8 == 0) 2029 | { 2030 | return true; 2031 | } 2032 | } 2033 | 2034 | // big and little endian UTF-32 encodings, with BOM 2035 | // application/xml 2036 | if (cc1 == 0x00 && cc2 == 0x00 && cc3 == 0xfe && cc4 == 0xff) 2037 | { 2038 | if (cc5 == 0 && cc6 == 0 && cc7 == 0 && c8 == '<' && cc9 == 0 2039 | && cc10 == 0 && cc11 == 0 && c12 == '?' && cc13 == 0 2040 | && cc14 == 0 && cc15 == 0 && c16 == 'x') 2041 | { 2042 | return true; 2043 | } 2044 | } 2045 | 2046 | // application/xml 2047 | if (cc1 == 0xff && cc2 == 0xfe && cc3 == 0x00 && cc4 == 0x00) 2048 | { 2049 | if (c5 == '<' && cc6 == 0 && cc7 == 0 && cc8 == 0 && c9 == '?' 2050 | && cc10 == 0 && cc11 == 0 && cc12 == 0 && c13 == 'x' 2051 | && cc14 == 0 && cc15 == 0 && cc16 == 0) 2052 | { 2053 | return true; 2054 | } 2055 | } 2056 | 2057 | // application/pdf (%PDF-) 2058 | if (cc1 == 37 && cc2 == 80 && cc3 == 68 && cc4 == 70 && cc5 == 45) 2059 | { 2060 | return true; 2061 | } 2062 | 2063 | // image/png 2064 | if ((cc1 == 137 && cc2 == 80 && cc3 == 78 && cc4 == 71 && cc5 == 13 2065 | && cc6 == 10 && cc7 == 26 && cc8 == 10) || 2066 | (cc1 == 194 && cc2 == 137 && cc3 == 80 && cc4 == 78 && cc5 == 71 && cc6 == 13 //Our embedded PNG+XML 2067 | && cc7 == 10 && cc8 == 26 && cc9 == 10)) 2068 | { 2069 | return true; 2070 | } 2071 | 2072 | // image/jpeg 2073 | if (cc1 == 0xFF && cc2 == 0xD8 && cc3 == 0xFF) 2074 | { 2075 | if (cc4 == 0xE0 || cc4 == 0xEE) 2076 | { 2077 | return true; 2078 | } 2079 | 2080 | /** 2081 | * File format used by digital cameras to store images. 2082 | * Exif Format can be read by any application supporting 2083 | * JPEG. Exif Spec can be found at: 2084 | * http://www.pima.net/standards/it10/PIMA15740/Exif_2-1.PDF 2085 | */ 2086 | if ((cc4 == 0xE1) && (c7 == 'E' && c8 == 'x' && c9 == 'i' 2087 | && c10 == 'f' && cc11 == 0)) 2088 | { 2089 | return true; 2090 | } 2091 | } 2092 | 2093 | // image/webp 2094 | if (cc1 == 0x52 && cc2 == 0x49 && cc3 == 0x46 && cc4 == 0x46) //RIFF 2095 | { 2096 | if (cc9 == 0x57 && cc10 == 0x45 && cc11 == 0x42 && cc12 == 0x50) //WEBP 2097 | { 2098 | return true; 2099 | } 2100 | } 2101 | 2102 | // vsdx, vssx (also zip, jar, odt, ods, odp, docx, xlsx, pptx, apk, aar) 2103 | if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x04) 2104 | { 2105 | return true; 2106 | } 2107 | else if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x06) 2108 | { 2109 | return true; 2110 | } 2111 | 2112 | // json 2113 | if (c1 == '{' || c1 == '[') 2114 | { 2115 | return true; 2116 | } 2117 | 2118 | // mxfile, mxlibrary, mxGraphModel 2119 | if (c1 == '<' && c2 == 'm' && c3 == 'x') 2120 | { 2121 | return true; 2122 | } 2123 | } 2124 | 2125 | return false; 2126 | }; 2127 | 2128 | function isConflict(origStat, stat) 2129 | { 2130 | return stat != null && origStat != null && stat.mtimeMs != origStat.mtimeMs; 2131 | }; 2132 | 2133 | function getDraftFileName(fileObject) 2134 | { 2135 | let filePath = fileObject.path; 2136 | let draftFileName = '', counter = 1, uniquePart = ''; 2137 | 2138 | do 2139 | { 2140 | draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT); 2141 | uniquePart = '_' + counter++; 2142 | } while (fs.existsSync(draftFileName)); 2143 | 2144 | return draftFileName; 2145 | }; 2146 | 2147 | async function getFileDrafts(fileObject) 2148 | { 2149 | let filePath = fileObject.path; 2150 | let draftsPaths = [], drafts = [], draftFileName, counter = 1, uniquePart = ''; 2151 | 2152 | do 2153 | { 2154 | draftsPaths.push(draftFileName); 2155 | draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT); 2156 | uniquePart = '_' + counter++; 2157 | } while (fs.existsSync(draftFileName)); //TODO this assume continuous drafts names 2158 | 2159 | //Port old draft files to new prefex 2160 | counter = 1; 2161 | uniquePart = ''; 2162 | let draftExists = false; 2163 | 2164 | do 2165 | { 2166 | draftFileName = path.join(path.dirname(filePath), OLD_DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT); 2167 | draftExists = fs.existsSync(draftFileName); 2168 | 2169 | if (draftExists) 2170 | { 2171 | const newDraftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT); 2172 | await fsProm.rename(draftFileName, newDraftFileName); 2173 | draftsPaths.push(newDraftFileName); 2174 | } 2175 | 2176 | uniquePart = '_' + counter++; 2177 | } while (draftExists); //TODO this assume continuous drafts names 2178 | 2179 | //Skip the first null element 2180 | for (let i = 1; i < draftsPaths.length; i++) 2181 | { 2182 | try 2183 | { 2184 | let stat = await fsProm.lstat(draftsPaths[i]); 2185 | drafts.push({data: await fsProm.readFile(draftsPaths[i], 'utf8'), 2186 | created: stat.ctimeMs, 2187 | modified: stat.mtimeMs, 2188 | path: draftsPaths[i]}); 2189 | } 2190 | catch (e){} // Ignore 2191 | } 2192 | 2193 | return drafts; 2194 | }; 2195 | 2196 | async function saveDraft(fileObject, data) 2197 | { 2198 | var draftFileName = fileObject.draftFileName || getDraftFileName(fileObject); 2199 | 2200 | if (!checkFileContent(data) || path.resolve(draftFileName).startsWith(appBaseDir)) 2201 | { 2202 | throw new Error('Invalid file data'); 2203 | } 2204 | else 2205 | { 2206 | await fsProm.writeFile(draftFileName, data, 'utf8'); 2207 | 2208 | if (isWin) 2209 | { 2210 | try 2211 | { 2212 | // Add Hidden attribute: 2213 | var child = spawn('attrib', ['+h', draftFileName]); 2214 | child.on('error', function(err) 2215 | { 2216 | console.log('hiding draft file error: ' + err); 2217 | }); 2218 | } catch(e) {} 2219 | } 2220 | 2221 | return draftFileName; 2222 | } 2223 | } 2224 | 2225 | async function saveFile(fileObject, data, origStat, overwrite, defEnc) 2226 | { 2227 | if (!checkFileContent(data) || path.resolve(fileObject.path).startsWith(appBaseDir)) 2228 | { 2229 | throw new Error('Invalid file data'); 2230 | } 2231 | 2232 | var retryCount = 0; 2233 | var backupCreated = false; 2234 | var bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT); 2235 | const oldBkpPath = path.join(path.dirname(fileObject.path), OLD_BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT); 2236 | var writeEnc = defEnc || fileObject.encoding; 2237 | 2238 | var writeFile = async function() 2239 | { 2240 | let fh; 2241 | 2242 | try 2243 | { 2244 | // O_SYNC is for sync I/O and reduce risk of file corruption 2245 | fh = await fsProm.open(fileObject.path, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC); 2246 | await fsProm.writeFile(fh, data, writeEnc); 2247 | await fh.sync(); // Flush to disk 2248 | } 2249 | finally 2250 | { 2251 | await fh?.close(); 2252 | } 2253 | 2254 | let stat2 = await fsProm.stat(fileObject.path); 2255 | // Workaround for possible writing errors is to check the written 2256 | // contents of the file and retry 3 times before showing an error 2257 | let writtenData = await fsProm.readFile(fileObject.path, writeEnc); 2258 | 2259 | if (data != writtenData) 2260 | { 2261 | retryCount++; 2262 | 2263 | if (retryCount < 3) 2264 | { 2265 | return await writeFile(); 2266 | } 2267 | else 2268 | { 2269 | throw new Error('all saving trials failed'); 2270 | } 2271 | } 2272 | else 2273 | { 2274 | //We'll keep the backup file in case the original file is corrupted. TODO When should we delete the backup file? 2275 | if (backupCreated) 2276 | { 2277 | //fs.unlink(bkpPath, (err) => {}); //Ignore errors! 2278 | 2279 | //Delete old backup file with old prefix 2280 | if (fs.existsSync(oldBkpPath)) 2281 | { 2282 | fs.unlink(oldBkpPath, (err) => {}); //Ignore errors 2283 | } 2284 | } 2285 | 2286 | return stat2; 2287 | } 2288 | }; 2289 | 2290 | async function doSaveFile(isNew) 2291 | { 2292 | if (enableStoreBkp && !isNew) 2293 | { 2294 | //Copy file to backup file (after conflict and stat is checked) 2295 | let bkpFh; 2296 | 2297 | try 2298 | { 2299 | //Use file read then write to open the backup file direct sync write to reduce the chance of file corruption 2300 | let fileContent = await fsProm.readFile(fileObject.path, writeEnc); 2301 | bkpFh = await fsProm.open(bkpPath, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC); 2302 | await fsProm.writeFile(bkpFh, fileContent, writeEnc); 2303 | await bkpFh.sync(); // Flush to disk 2304 | backupCreated = true; 2305 | } 2306 | catch (e) 2307 | { 2308 | if (__DEV__) 2309 | { 2310 | console.log('Backup file writing failed', e); //Ignore 2311 | } 2312 | } 2313 | finally 2314 | { 2315 | await bkpFh?.close(); 2316 | 2317 | if (isWin) 2318 | { 2319 | try 2320 | { 2321 | // Add Hidden attribute: 2322 | var child = spawn('attrib', ['+h', bkpPath]); 2323 | child.on('error', function(err) 2324 | { 2325 | console.log('hiding backup file error: ' + err); 2326 | }); 2327 | } catch(e) {} 2328 | } 2329 | } 2330 | } 2331 | 2332 | return await writeFile(); 2333 | }; 2334 | 2335 | if (overwrite) 2336 | { 2337 | return await doSaveFile(true); 2338 | } 2339 | else 2340 | { 2341 | let stat = fs.existsSync(fileObject.path)? 2342 | await fsProm.stat(fileObject.path) : null; 2343 | 2344 | if (stat && isConflict(origStat, stat)) 2345 | { 2346 | throw new Error('conflict'); 2347 | } 2348 | else 2349 | { 2350 | return await doSaveFile(stat == null); 2351 | } 2352 | } 2353 | }; 2354 | 2355 | async function writeFile(filePath, data, enc) 2356 | { 2357 | if (!checkFileContent(data, enc) || path.resolve(filePath).startsWith(appBaseDir)) 2358 | { 2359 | throw new Error('Invalid file data'); 2360 | } 2361 | else 2362 | { 2363 | let fh; 2364 | 2365 | try 2366 | { 2367 | // O_SYNC is for sync I/O and reduce risk of file corruption 2368 | fh = await fsProm.open(filePath, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC); 2369 | await fsProm.writeFile(fh, data, enc); 2370 | await fh.sync(); // Flush to disk 2371 | } 2372 | finally 2373 | { 2374 | await fh?.close(); 2375 | } 2376 | } 2377 | }; 2378 | 2379 | function getAppDataFolder() 2380 | { 2381 | try 2382 | { 2383 | var appDataDir = app.getPath('appData'); 2384 | var drawioDir = appDataDir + '/draw.io'; 2385 | 2386 | if (!fs.existsSync(drawioDir)) //Usually this dir already exists 2387 | { 2388 | fs.mkdirSync(drawioDir); 2389 | } 2390 | 2391 | return drawioDir; 2392 | } 2393 | catch(e) {} 2394 | 2395 | return '.'; 2396 | }; 2397 | 2398 | function getDocumentsFolder() 2399 | { 2400 | //On windows, misconfigured Documents folder cause an exception 2401 | try 2402 | { 2403 | return app.getPath('documents'); 2404 | } 2405 | catch(e) {} 2406 | 2407 | return '.'; 2408 | }; 2409 | 2410 | function checkFileExists(pathParts) 2411 | { 2412 | let filePath = path.join(...pathParts); 2413 | return {exists: fs.existsSync(filePath), path: filePath}; 2414 | }; 2415 | 2416 | async function showOpenDialog(defaultPath, filters, properties) 2417 | { 2418 | let win = BrowserWindow.getFocusedWindow(); 2419 | 2420 | return dialog.showOpenDialog(win, { 2421 | defaultPath: defaultPath, 2422 | filters: filters, 2423 | properties: properties 2424 | }); 2425 | }; 2426 | 2427 | async function showSaveDialog(defaultPath, filters) 2428 | { 2429 | let win = BrowserWindow.getFocusedWindow(); 2430 | 2431 | return dialog.showSaveDialog(win, { 2432 | defaultPath: defaultPath, 2433 | filters: filters 2434 | }); 2435 | }; 2436 | 2437 | async function installPlugin(filePath) 2438 | { 2439 | if (!enablePlugins) return {}; 2440 | 2441 | var pluginsDir = path.join(getAppDataFolder(), '/plugins'); 2442 | 2443 | if (!fs.existsSync(pluginsDir)) 2444 | { 2445 | fs.mkdirSync(pluginsDir); 2446 | } 2447 | 2448 | var pluginName = path.basename(filePath); 2449 | var dstFile = path.join(pluginsDir, pluginName); 2450 | 2451 | if (fs.existsSync(dstFile)) 2452 | { 2453 | throw new Error('fileExists'); 2454 | } 2455 | else 2456 | { 2457 | await fsProm.copyFile(filePath, dstFile); 2458 | } 2459 | 2460 | return {pluginName: pluginName, selDir: path.dirname(filePath)}; 2461 | } 2462 | 2463 | function getPluginFile(plugin) 2464 | { 2465 | if (!enablePlugins) return null; 2466 | 2467 | const prefix = path.join(getAppDataFolder(), '/plugins/'); 2468 | const pluginFile = path.join(prefix, plugin); 2469 | 2470 | if (pluginFile.startsWith(prefix) && fs.existsSync(pluginFile)) 2471 | { 2472 | return pluginFile; 2473 | } 2474 | 2475 | return null; 2476 | } 2477 | 2478 | function uninstallPlugin(plugin) 2479 | { 2480 | const pluginFile = getPluginFile(plugin); 2481 | 2482 | if (pluginFile != null) 2483 | { 2484 | fs.unlinkSync(pluginFile); 2485 | } 2486 | } 2487 | 2488 | function dirname(path_p) 2489 | { 2490 | return path.dirname(path_p); 2491 | } 2492 | 2493 | async function readFile(filename, encoding) 2494 | { 2495 | let data = await fsProm.readFile(filename, encoding); 2496 | 2497 | if (checkFileContent(data, encoding) && !path.resolve(filename).startsWith(appBaseDir)) 2498 | { 2499 | return data; 2500 | } 2501 | 2502 | throw new Error('Invalid file data'); 2503 | } 2504 | 2505 | async function fileStat(file) 2506 | { 2507 | return await fsProm.stat(file); 2508 | } 2509 | 2510 | async function isFileWritable(file) 2511 | { 2512 | try 2513 | { 2514 | await fsProm.access(file, fs.constants.W_OK); 2515 | return true; 2516 | } 2517 | catch (e) 2518 | { 2519 | return false; 2520 | } 2521 | } 2522 | 2523 | function clipboardAction(method, data) 2524 | { 2525 | if (method == 'writeText') 2526 | { 2527 | clipboard.writeText(data); 2528 | } 2529 | else if (method == 'readText') 2530 | { 2531 | return clipboard.readText(); 2532 | } 2533 | else if (method == 'writeImage') 2534 | { 2535 | clipboard.write({image: 2536 | nativeImage.createFromDataURL(data.dataUrl), html: ''}); 2538 | } 2539 | } 2540 | 2541 | async function deleteFile(file) 2542 | { 2543 | // Reading the header of the file to confirm it is a file we can delete 2544 | let fh = await fsProm.open(file, O_RDONLY); 2545 | let buffer = Buffer.allocUnsafe(16); 2546 | await fh.read(buffer, 0, 16); 2547 | await fh.close(); 2548 | 2549 | if (checkFileContent(buffer) && !path.resolve(file).startsWith(appBaseDir)) 2550 | { 2551 | await fsProm.unlink(file); 2552 | } 2553 | } 2554 | 2555 | function windowAction(method) 2556 | { 2557 | let win = BrowserWindow.getFocusedWindow(); 2558 | 2559 | if (win) 2560 | { 2561 | if (method == 'minimize') 2562 | { 2563 | win.minimize(); 2564 | } 2565 | else if (method == 'maximize') 2566 | { 2567 | win.maximize(); 2568 | } 2569 | else if (method == 'unmaximize') 2570 | { 2571 | win.unmaximize(); 2572 | } 2573 | else if (method == 'close') 2574 | { 2575 | win.close(); 2576 | } 2577 | else if (method == 'isMaximized') 2578 | { 2579 | return win.isMaximized(); 2580 | } 2581 | else if (method == 'removeAllListeners') 2582 | { 2583 | win.removeAllListeners(); 2584 | } 2585 | } 2586 | } 2587 | 2588 | const allowedUrls = /^(?:https?|mailto|tel|callto):/i; 2589 | 2590 | function openExternal(url) 2591 | { 2592 | //Only open http(s), mailto, tel, and callto links 2593 | if (allowedUrls.test(url)) 2594 | { 2595 | shell.openExternal(url); 2596 | return true; 2597 | } 2598 | 2599 | return false; 2600 | } 2601 | 2602 | function watchFile(filePath) 2603 | { 2604 | let win = BrowserWindow.getFocusedWindow(); 2605 | 2606 | if (win) 2607 | { 2608 | fs.watchFile(filePath, (curr, prev) => { 2609 | try 2610 | { 2611 | win.webContents.send('fileChanged', { 2612 | path: filePath, 2613 | curr: curr, 2614 | prev: prev 2615 | }); 2616 | } 2617 | catch (e) {} // Ignore 2618 | }); 2619 | } 2620 | } 2621 | 2622 | function unwatchFile(filePath) 2623 | { 2624 | fs.unwatchFile(filePath); 2625 | } 2626 | 2627 | ipcMain.on("rendererReq", async (event, args) => 2628 | { 2629 | if (!validateSender(event.senderFrame)) return null; 2630 | 2631 | try 2632 | { 2633 | let ret = null; 2634 | 2635 | switch(args.action) 2636 | { 2637 | case 'saveFile': 2638 | ret = await saveFile(args.fileObject, args.data, args.origStat, args.overwrite, args.defEnc); 2639 | break; 2640 | case 'writeFile': 2641 | ret = await writeFile(args.path, args.data, args.enc); 2642 | break; 2643 | case 'saveDraft': 2644 | ret = await saveDraft(args.fileObject, args.data); 2645 | break; 2646 | case 'getFileDrafts': 2647 | ret = await getFileDrafts(args.fileObject); 2648 | break; 2649 | case 'getDocumentsFolder': 2650 | ret = await getDocumentsFolder(); 2651 | break; 2652 | case 'checkFileExists': 2653 | ret = await checkFileExists(args.pathParts); 2654 | break; 2655 | case 'showOpenDialog': 2656 | dialogOpen = true; 2657 | ret = await showOpenDialog(args.defaultPath, args.filters, args.properties); 2658 | ret = ret.filePaths; 2659 | dialogOpen = false; 2660 | break; 2661 | case 'showSaveDialog': 2662 | dialogOpen = true; 2663 | ret = await showSaveDialog(args.defaultPath, args.filters); 2664 | ret = ret.canceled? null : ret.filePath; 2665 | dialogOpen = false; 2666 | break; 2667 | case 'installPlugin': 2668 | ret = await installPlugin(args.filePath); 2669 | break; 2670 | case 'uninstallPlugin': 2671 | ret = await uninstallPlugin(args.plugin); 2672 | break; 2673 | case 'getPluginFile': 2674 | ret = await getPluginFile(args.plugin); 2675 | break; 2676 | case 'isPluginsEnabled': 2677 | ret = enablePlugins; 2678 | break; 2679 | case 'dirname': 2680 | ret = await dirname(args.path); 2681 | break; 2682 | case 'readFile': 2683 | ret = await readFile(args.filename, args.encoding); 2684 | break; 2685 | case 'clipboardAction': 2686 | ret = await clipboardAction(args.method, args.data); 2687 | break; 2688 | case 'deleteFile': 2689 | ret = await deleteFile(args.file); 2690 | break; 2691 | case 'fileStat': 2692 | ret = await fileStat(args.file); 2693 | break; 2694 | case 'isFileWritable': 2695 | ret = await isFileWritable(args.file); 2696 | break; 2697 | case 'windowAction': 2698 | ret = await windowAction(args.method); 2699 | break; 2700 | case 'openExternal': 2701 | ret = await openExternal(args.url); 2702 | break; 2703 | case 'watchFile': 2704 | ret = await watchFile(args.path); 2705 | break; 2706 | case 'unwatchFile': 2707 | ret = await unwatchFile(args.path); 2708 | break; 2709 | case 'exit': 2710 | app.quit(); 2711 | break; 2712 | case 'isFullscreen': 2713 | ret = BrowserWindow.getFocusedWindow().isFullScreen(); 2714 | break; 2715 | }; 2716 | 2717 | event.reply('mainResp', {success: true, data: ret, reqId: args.reqId}); 2718 | } 2719 | catch (e) 2720 | { 2721 | event.reply('mainResp', {error: true, msg: e.message, e: e, reqId: args.reqId}); 2722 | } 2723 | }); --------------------------------------------------------------------------------