first-commit
0 parents
Showing
83 changed files
with
5081 additions
and
0 deletions
.editorconfig
0 → 100644
.eslintignore
0 → 100644
.eslintrc.js
0 → 100644
.gitattributes
0 → 100644
| 1 | # Set the default behavior, in case people don't have core.autocrlf set. | ||
| 2 | * text=auto | ||
| 3 | |||
| 4 | # Explicitly specify line endings for as many files as possible. | ||
| 5 | # People who (for example) rsync between Windows and Linux need this. | ||
| 6 | |||
| 7 | # File types which we know are binary | ||
| 8 | |||
| 9 | # Prefer LF for most file types | ||
| 10 | *.css text eol=lf | ||
| 11 | *.htm text eol=lf | ||
| 12 | *.html text eol=lf | ||
| 13 | *.js text eol=lf | ||
| 14 | *.js.map text eol=lf | ||
| 15 | *.json text eol=lf | ||
| 16 | *.json5 text eol=lf | ||
| 17 | *.jsx text eol=lf | ||
| 18 | *.md text eol=lf | ||
| 19 | *.plist text eol=lf | ||
| 20 | *.xml text eol=lf | ||
| 21 | *.yml text eol=lf | ||
| 22 | *.yaml text eol=lf | ||
| 23 | |||
| 24 | # Prefer LF for these files | ||
| 25 | .editorconfig text eol=lf | ||
| 26 | .eslintignore text eol=lf | ||
| 27 | .gitattributes text eol=lf | ||
| 28 | .gitignore text eol=lf | ||
| 29 | .npmignore text eol=lf | ||
| 30 | LICENSE text eol=lf | ||
| 31 | Makefile text eol=lf | ||
| 32 | TRADEMARK text eol=lf | ||
| 33 | |||
| 34 | # Use CRLF for Windows-specific file types |
.github/CONTRIBUTING.md
0 → 100644
| 1 | ## Contributing | ||
| 2 | The development of Scratch is an ongoing process, and we love to have people in the Scratch and open source communities help us along the way. | ||
| 3 | |||
| 4 | ### Ways to Help | ||
| 5 | |||
| 6 | * **Documenting bugs** | ||
| 7 | * If you've identified a bug in Scratch you should first check to see if it's been filed as an issue, if not you can file one. Make sure you follow the issue template. | ||
| 8 | * It's important that we can consistently reproduce issues. When writing an issue, be sure to follow our [reproduction step guidelines](https://github.com/LLK/scratch-gui/wiki/Writing-good-repro-steps). | ||
| 9 | * Some issues are marked "Needs Repro". Adding a comment with good reproduction steps to those issues is a great way to help. | ||
| 10 | * If you don't have an issue in mind already, you can look through the [Bugs & Glitches forum.](https://scratch.mit.edu/discuss/3/) Look for users reporting problems, reproduce the problem yourself, and file new issues following our guidelines. | ||
| 11 | |||
| 12 | * **Fixing bugs** | ||
| 13 | * You can request to fix a bug in a comment on the issue if you at mention the repo coordinator, who for this repo is @cwillisf. | ||
| 14 | * If the issue is marked "Help Wanted" you can go ahead and start working on it! | ||
| 15 | * **We will only accept Pull Requests for bugs that have an issue filed that has a priority label** | ||
| 16 | * If you're interested in fixing a bug with no issue, file the issue first and wait for it to have a priority added to it. | ||
| 17 | |||
| 18 | * We are not looking for Pull Requests ("PR") for every issue and may deny a PR if it doesn't fit our criteria. | ||
| 19 | * We are far more likely to accept a PR if it is for an issue marked with Help Wanted. | ||
| 20 | * We will not accept PRs for issues marked with "Needs Discussion" or "Needs Design." | ||
| 21 | * Wait until the Repo Coordinator assigns the issue to you before you begin work or submit a PR. | ||
| 22 | |||
| 23 | ### Learning Git and Github | ||
| 24 | |||
| 25 | If you want to work on fixing issues, you should be familiar with Git and Github. | ||
| 26 | |||
| 27 | * [Learn Git branching](https://learngitbranching.js.org/) includes an introduction to basic git commands and useful branching features. | ||
| 28 | * Here's a general introduction to [contributing to an open source project](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). | ||
| 29 | |||
| 30 | **Important:** we follow the [Github Flow process](https://guides.github.com/introduction/flow/) as our development process. | ||
| 31 | |||
| 32 | ### How to Fix Bugs | ||
| 33 | 1. Identify which Github issue you are working on. Leave a comment on the issue to let us (and other contributors) know you're working on it. | ||
| 34 | 2. Make sure you have a fork of this repo (see [Github's forking a repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) for details) | ||
| 35 | 3. Switch to the `develop` branch, and pull down the latest changes from upstream | ||
| 36 | 4. Run the code, and reproduce the problem | ||
| 37 | 5. Create your branch from the `develop` branch | ||
| 38 | 6. Make code changes to fix the problem | ||
| 39 | 7. Run `npm test` to make sure that your changes pass our tests | ||
| 40 | 8. Commit your changes | ||
| 41 | 9. Push your branch to your fork | ||
| 42 | 10. Create your pull request | ||
| 43 | 1. Make sure to follow the template in the PR description | ||
| 44 | 1. Remember to check the “[Allow edits from maintainers](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)” box | ||
| 45 | |||
| 46 | When submitting pull requests keep in mind: | ||
| 47 | * please be patient -- it can take a while to find time to review them | ||
| 48 | * try to change the least amount of code necessary to fix the bug | ||
| 49 | * the code can't be radically changed without significant coordination with the Scratch Team, so these types of changes should be avoided | ||
| 50 | * if you find yourself changing a substantial amount of code or considering radical changes, please ask for clarification -- we may have envisioned a different approach, or underestimated the amount of effort | ||
| 51 | |||
| 52 | ### Suggestions | ||
| 53 |  | ||
| 54 | |||
| 55 | Please note: **_we are unlikely to accept PRs with new features that haven't been thought through and discussed as a group_**. | ||
| 56 | |||
| 57 | Why? Because we have a strong belief in the value of keeping things simple for new users. It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch. To learn more about our design philosophy, see [the Scratch Developers page](https://scratch.mit.edu/developers), or [this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf). | ||
| 58 | |||
| 59 | We welcome suggestions! If you want to suggest a feature, please post in our [suggestions forum](https://scratch.mit.edu/discuss/1/). Your suggestion will be helped if you include a mockup design; this can be simple, even hand-drawn. | ||
| 60 | |||
| 61 | ### Other resources | ||
| 62 | Beyond this repo, there are also some other resources that you might want to take a look at: | ||
| 63 | * [Community Guidelines](https://github.com/LLK/scratch-www/wiki/Community-Guidelines) (we find it important to maintain a constructive and welcoming community, just like on Scratch) | ||
| 64 | * [Open Source forum](https://scratch.mit.edu/discuss/49/) on Scratch | ||
| 65 | * [Suggestions forum](https://scratch.mit.edu/discuss/1/) on Scratch | ||
| 66 | * [Bugs & Glitches forum](https://scratch.mit.edu/discuss/3/) on Scratch |
.github/FUNDING.yml
0 → 100644
| 1 | ko_fi: arthurzheng |
.github/ISSUE_TEMPLATE.md
0 → 100644
| 1 | ### Expected Behavior | ||
| 2 | |||
| 3 | _Please describe what should happen_ | ||
| 4 | |||
| 5 | ### Actual Behavior | ||
| 6 | |||
| 7 | _Describe what actually happens_ | ||
| 8 | |||
| 9 | ### Steps to Reproduce | ||
| 10 | |||
| 11 | _Explain what someone needs to do in order to see what's described in *Actual behavior* above_ | ||
| 12 | |||
| 13 | ### Operating System and Browser | ||
| 14 | |||
| 15 | _e.g. Mac OS 10.11.6 Safari 10.0_ |
.github/PULL_REQUEST_TEMPLATE.md
0 → 100644
| 1 | ### Resolves | ||
| 2 | |||
| 3 | _What Github issue does this resolve (please include link)?_ | ||
| 4 | |||
| 5 | - Resolves # | ||
| 6 | |||
| 7 | ### Proposed Changes | ||
| 8 | |||
| 9 | _Describe what this Pull Request does_ | ||
| 10 | |||
| 11 | ### Reason for Changes | ||
| 12 | |||
| 13 | _Explain why these changes should be made_ | ||
| 14 | |||
| 15 | ### Test Coverage | ||
| 16 | |||
| 17 | _Please show how you have added tests to cover your changes_ |
.github/workflows/build-and-release.yml
0 → 100644
| 1 | name: Build and release | ||
| 2 | |||
| 3 | on: | ||
| 4 | push: | ||
| 5 | tags: | ||
| 6 | - 'v*' | ||
| 7 | paths-ignore: | ||
| 8 | - 'README.md' | ||
| 9 | - 'CHANGE.md' | ||
| 10 | - 'doc/*' | ||
| 11 | - '.github/*' | ||
| 12 | |||
| 13 | jobs: | ||
| 14 | create-release: | ||
| 15 | runs-on: ubuntu-20.04 | ||
| 16 | steps: | ||
| 17 | - uses: actions/checkout@v2 | ||
| 18 | |||
| 19 | - name: Get tag | ||
| 20 | id: tag | ||
| 21 | uses: dawidd6/action-get-tag@v1 | ||
| 22 | |||
| 23 | - name: Create Release | ||
| 24 | id: create_release | ||
| 25 | uses: actions/create-release@v1 | ||
| 26 | env: | ||
| 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| 28 | with: | ||
| 29 | tag_name: ${{ steps.tag.outputs.tag }} | ||
| 30 | release_name: OpenBlock Desktop ${{ steps.tag.outputs.tag }} | ||
| 31 | body: | | ||
| 32 | ## Change Log (en) | ||
| 33 | |||
| 34 | - **New feature** | ||
| 35 | |||
| 36 | 1. Feature 1. | ||
| 37 | 2. Feature 2. | ||
| 38 | |||
| 39 | - **Fix bug** | ||
| 40 | |||
| 41 | 1. Bug 1. | ||
| 42 | 2. Bug 2. | ||
| 43 | |||
| 44 | ## 更改日志 (zh-cn) | ||
| 45 | |||
| 46 | - **新功能** | ||
| 47 | |||
| 48 | 1. 功能1。 | ||
| 49 | 2. 功能2。 | ||
| 50 | |||
| 51 | - **修复错误** | ||
| 52 | |||
| 53 | 1. 错误1。 | ||
| 54 | 2. 错误2。 | ||
| 55 | draft: true | ||
| 56 | prerelease: false | ||
| 57 | |||
| 58 | build-windows: | ||
| 59 | needs: create-release | ||
| 60 | runs-on: windows-2019 | ||
| 61 | steps: | ||
| 62 | - uses: actions/checkout@v2 | ||
| 63 | |||
| 64 | - name: Use Node.js | ||
| 65 | uses: actions/setup-node@v2 | ||
| 66 | with: | ||
| 67 | node-version: '14.x' | ||
| 68 | |||
| 69 | - name: Npm Install | ||
| 70 | run: | | ||
| 71 | git config --global url."https://github.com".insteadOf ssh://git@github.com | ||
| 72 | npm ci | ||
| 73 | |||
| 74 | - name: Build and Publish | ||
| 75 | env: | ||
| 76 | GA_ID: ${{ secrets.GA_ID }} | ||
| 77 | GH_TOKEN: ${{ secrets.GH_TOKEN }} | ||
| 78 | NODE_ENV: production | ||
| 79 | NODE_OPTIONS: --max_old_space_size=4096 | ||
| 80 | run: npm run publish | ||
| 81 | |||
| 82 | build-mac: | ||
| 83 | needs: create-release | ||
| 84 | runs-on: macos-13 | ||
| 85 | steps: | ||
| 86 | - uses: actions/checkout@v2 | ||
| 87 | |||
| 88 | - name: Use Node.js | ||
| 89 | uses: actions/setup-node@v2 | ||
| 90 | with: | ||
| 91 | node-version: '16.x' | ||
| 92 | |||
| 93 | - name: Install python2.7 | ||
| 94 | run: | | ||
| 95 | brew install pyenv | ||
| 96 | pyenv install 2.7.18 | ||
| 97 | pyenv global 2.7.18 | ||
| 98 | |||
| 99 | - name: Npm Install | ||
| 100 | run: | | ||
| 101 | pip install setuptools | ||
| 102 | npm ci | ||
| 103 | |||
| 104 | - name: Build and Publish | ||
| 105 | env: | ||
| 106 | GA_ID: ${{ secrets.GA_ID }} | ||
| 107 | GH_TOKEN: ${{ secrets.GH_TOKEN }} | ||
| 108 | NODE_ENV: production | ||
| 109 | NODE_OPTIONS: --max_old_space_size=8192 | ||
| 110 | CSC_IDENTITY_AUTO_DISCOVERY: false | ||
| 111 | PYTHON_PATH: python | ||
| 112 | run: | | ||
| 113 | export PYTHON_PATH=$(pyenv root)/shims/python | ||
| 114 | npm run publish | ||
| 115 | |||
| 116 | build-linux: | ||
| 117 | needs: create-release | ||
| 118 | runs-on: ubuntu-20.04 | ||
| 119 | steps: | ||
| 120 | - uses: actions/checkout@v2 | ||
| 121 | |||
| 122 | - name: Use Node.js | ||
| 123 | uses: actions/setup-node@v2 | ||
| 124 | with: | ||
| 125 | node-version: '16.x' | ||
| 126 | |||
| 127 | - name: Npm Install | ||
| 128 | run: npm ci | ||
| 129 | |||
| 130 | - name: Build and Publish | ||
| 131 | env: | ||
| 132 | GA_ID: ${{ secrets.GA_ID }} | ||
| 133 | GH_TOKEN: ${{ secrets.GH_TOKEN }} | ||
| 134 | NODE_ENV: production | ||
| 135 | NODE_OPTIONS: --max_old_space_size=8192 | ||
| 136 | CSC_IDENTITY_AUTO_DISCOVERY: false | ||
| 137 | run: npm run publish |
.github/workflows/build.yml
0 → 100644
| 1 | name: Build Test App | ||
| 2 | |||
| 3 | on: | ||
| 4 | push: | ||
| 5 | branches: [ main ] | ||
| 6 | paths-ignore: | ||
| 7 | - 'README.md' | ||
| 8 | - 'CHANGE.md' | ||
| 9 | - 'doc/*' | ||
| 10 | - '.github/*' | ||
| 11 | |||
| 12 | jobs: | ||
| 13 | build-windows: | ||
| 14 | runs-on: windows-2019 | ||
| 15 | steps: | ||
| 16 | - uses: actions/checkout@v2 | ||
| 17 | |||
| 18 | - name: Use Node.js | ||
| 19 | uses: actions/setup-node@v2 | ||
| 20 | with: | ||
| 21 | node-version: '14.x' | ||
| 22 | |||
| 23 | - name: Npm Install | ||
| 24 | run: | | ||
| 25 | git config --global url."https://github.com".insteadOf ssh://git@github.com | ||
| 26 | npm ci | ||
| 27 | |||
| 28 | - name: Build Dist | ||
| 29 | env: | ||
| 30 | GA_ID: ${{ secrets.GA_ID }} | ||
| 31 | NODE_ENV: production | ||
| 32 | NODE_OPTIONS: --max_old_space_size=4096 | ||
| 33 | run: npm run dist | ||
| 34 | |||
| 35 | - name: Archive production artifacts | ||
| 36 | uses: actions/upload-artifact@v2 | ||
| 37 | with: | ||
| 38 | name: OpenBlock-Desktop-win32 | ||
| 39 | path: dist/OpenBlock-Desktop*.exe | ||
| 40 | retention-days: 1 | ||
| 41 | |||
| 42 | build-mac: | ||
| 43 | runs-on: macos-13 | ||
| 44 | steps: | ||
| 45 | - uses: actions/checkout@v2 | ||
| 46 | |||
| 47 | - name: Use Node.js | ||
| 48 | uses: actions/setup-node@v2 | ||
| 49 | with: | ||
| 50 | node-version: '16.x' | ||
| 51 | |||
| 52 | - name: Install python2.7 | ||
| 53 | run: | | ||
| 54 | brew install pyenv | ||
| 55 | pyenv install 2.7.18 | ||
| 56 | pyenv global 2.7.18 | ||
| 57 | |||
| 58 | - name: Npm Install | ||
| 59 | run: | | ||
| 60 | pip install setuptools | ||
| 61 | npm ci | ||
| 62 | |||
| 63 | - name: Build Dist | ||
| 64 | env: | ||
| 65 | GA_ID: ${{ secrets.GA_ID }} | ||
| 66 | NODE_ENV: production | ||
| 67 | NODE_OPTIONS: --max_old_space_size=8192 | ||
| 68 | CSC_IDENTITY_AUTO_DISCOVERY: false | ||
| 69 | run: | | ||
| 70 | export PYTHON_PATH=$(pyenv root)/shims/python | ||
| 71 | npm run dist | ||
| 72 | |||
| 73 | - name: Archive production artifacts | ||
| 74 | uses: actions/upload-artifact@v2 | ||
| 75 | with: | ||
| 76 | name: OpenBlock-Desktop-darwin | ||
| 77 | path: dist/OpenBlock-Desktop*.dmg | ||
| 78 | retention-days: 1 | ||
| 79 | |||
| 80 | build-linux: | ||
| 81 | runs-on: ubuntu-20.04 | ||
| 82 | steps: | ||
| 83 | - uses: actions/checkout@v2 | ||
| 84 | |||
| 85 | - name: Use Node.js | ||
| 86 | uses: actions/setup-node@v2 | ||
| 87 | with: | ||
| 88 | node-version: '16.x' | ||
| 89 | |||
| 90 | - name: Npm Install | ||
| 91 | run: npm ci | ||
| 92 | |||
| 93 | - name: Build Dist | ||
| 94 | env: | ||
| 95 | GA_ID: ${{ secrets.GA_ID }} | ||
| 96 | NODE_ENV: production | ||
| 97 | NODE_OPTIONS: --max_old_space_size=8192 | ||
| 98 | CSC_IDENTITY_AUTO_DISCOVERY: false | ||
| 99 | run: npm run dist | ||
| 100 | |||
| 101 | - name: Archive production artifacts | ||
| 102 | uses: actions/upload-artifact@v2 | ||
| 103 | with: | ||
| 104 | name: OpenBlock-Desktop-linux | ||
| 105 | path: dist/OpenBlock-Desktop*.deb | ||
| 106 | retention-days: 1 |
.gitignore
0 → 100644
| 1 | # Mac OS | ||
| 2 | .DS_Store | ||
| 3 | |||
| 4 | # Windows | ||
| 5 | thumbs.db | ||
| 6 | |||
| 7 | # NPM | ||
| 8 | /node_modules | ||
| 9 | npm-* | ||
| 10 | |||
| 11 | # Testing | ||
| 12 | /.nyc_output | ||
| 13 | /coverage | ||
| 14 | .eslintcache | ||
| 15 | |||
| 16 | # Build | ||
| 17 | /build | ||
| 18 | /dist | ||
| 19 | /.opt-in | ||
| 20 | /*.provisionprofile | ||
| 21 | |||
| 22 | # don't store the assets downloaded with the `fetch` script | ||
| 23 | /static | ||
| 24 | |||
| 25 | # Temporary resource | ||
| 26 | /external-resources | ||
| 27 | /tools | ||
| 28 | /firmwares | ||
| 29 | /drivers | ||
| 30 | |||
| 31 | |||
| 32 | # generated translation files | ||
| 33 | /translations | ||
| 34 | /locale |
.npmignore
0 → 100644
.vscode/launch.json
0 → 100644
| 1 | { | ||
| 2 | // 使用 IntelliSense 了解相关属性。 | ||
| 3 | // 悬停以查看现有属性的描述。 | ||
| 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 | ||
| 5 | "version": "0.2.0", | ||
| 6 | "configurations": [ | ||
| 7 | { | ||
| 8 | "name": "Desktop", | ||
| 9 | "type": "node", | ||
| 10 | "request": "launch", | ||
| 11 | "cwd": "${workspaceFolder:scratch-desktop}", | ||
| 12 | "runtimeExecutable": "npm", | ||
| 13 | "autoAttachChildProcesses": true, | ||
| 14 | "runtimeArgs": ["start", "--"], | ||
| 15 | "protocol": "inspector", | ||
| 16 | "skipFiles": [ | ||
| 17 | // it seems like skipFiles only reliably works with 1 entry :( | ||
| 18 | //"<node_internals>/**", | ||
| 19 | "${workspaceFolder:scratch-desktop}/node_modules/electron/dist/resources/*.asar/**" | ||
| 20 | ], | ||
| 21 | "sourceMaps": true, | ||
| 22 | "timeout": 30000, | ||
| 23 | "outputCapture": "std" | ||
| 24 | } | ||
| 25 | ] | ||
| 26 | } |
CHANGE.md
0 → 100644
| 1 | # Change Log | ||
| 2 | |||
| 3 | ## 2.5.2 | ||
| 4 | |||
| 5 | - **New feature** | ||
| 6 | |||
| 7 | 1. Supports using environment variables to specify the path of external resources. | ||
| 8 | |||
| 9 | - **Fix bug** | ||
| 10 | |||
| 11 | 1. Fixed the issue where some window images were lost and could not be displayed. | ||
| 12 | 2. Fix the problem that the resources library file cannot be found. | ||
| 13 | |||
| 14 | ## 2.5.1 | ||
| 15 | |||
| 16 | - **New feature** | ||
| 17 | |||
| 18 | 1. Re-support windows-ia32 architecture. | ||
| 19 | |||
| 20 | - **Fix bug** | ||
| 21 | |||
| 22 | 1. When installing on Windows, an error message always appears: The app is running and cannot be closed. | ||
| 23 | 2. Running on Windows 7 will report an error and cannot start. | ||
| 24 | |||
| 25 | ## 2.5.0 | ||
| 26 | |||
| 27 | - **New feature** | ||
| 28 | |||
| 29 | 1. Added support for Russian language. | ||
| 30 | 2. Add support for unload sratch extennsion. | ||
| 31 | 3. When the window width cannot fit the menu bar content, an abbreviated version of the menu is displayed. | ||
| 32 | 4. Adjust the framework of external resources and support the injection of resources from non-installation departments. | ||
| 33 | 5. Add support for copying and pasting blocks. | ||
| 34 | 6. Add support for custom matrix block height and width. | ||
| 35 | 7. Added support for aborting the Arduino upload process. | ||
| 36 | 8. Upgrade google analytics to GA4. | ||
| 37 | |||
| 38 | - **Fix bug** | ||
| 39 | |||
| 40 | 1. After unchecking show all devices, non-compliant devices still appear in the connection list. | ||
| 41 | 2. After uploading is completed, click the upload information to automatically jump to the bottom of the content. | ||
| 42 | 3. Corrected the text description of the Arduino serial port reading block. | ||
| 43 | 4. Corrected the code generation structure of the microbit event block to prevent function crash caused by repeated reentry when using a perpetual loop in the event block. | ||
| 44 | 5. The Arduino program will be stuck for a short while before uploading | ||
| 45 | 6. esp32 arduino setPinMode building block pin menu only shows output pins. | ||
| 46 | 7. In the library interface, if a item title is too long, it will not be fully displayed. | ||
| 47 | 8. mega2560 real-time mode reading analog pin error. | ||
| 48 | |||
| 49 | ## 2.4.1 | ||
| 50 | |||
| 51 | - **New feature** | ||
| 52 | |||
| 53 | 1. Designate independent cache paths for different Arduino boards to speed up compilation. | ||
| 54 | 2. Add the upload abort function to abort the upload operation during the upload process. | ||
| 55 | 3. When the device is being connected, if the serial port is occupied, it will display the connection failure and prompt that the serial port is occupied. | ||
| 56 | 4. A 3-second timer is added to automatically close the upload window when the upload is successful or aborted. The user can click any blank space or press ESC before the timer expires to stop the timer to check the log in the upload window. | ||
| 57 | 5. Adjust the default size after the interface is opened, so that the extension interface can fully display 5 lines of content. | ||
| 58 | |||
| 59 | - **Fix bug** | ||
| 60 | |||
| 61 | 1. After installing for the first time or clearing the cache and restarting, the device blocks are all damaged when loading a project with device. | ||
| 62 | 2. Get error when trying to save a project into the root path of drive. | ||
| 63 | |||
| 64 | ## 2.4.0 | ||
| 65 | |||
| 66 | - **New feature** | ||
| 67 | |||
| 68 | 1. Add support for Linux system. | ||
| 69 | 2. Add support for Arduino Raspberry Pi Pico. | ||
| 70 | |||
| 71 | - **Fix bug** | ||
| 72 | |||
| 73 | 1. After load project file, the custom blocks in the project is missing in toolbox. | ||
| 74 | |||
| 75 | ## v2.3.3 | ||
| 76 | |||
| 77 | - **New feature** | ||
| 78 | |||
| 79 | 1. Hide the unspported variable blocks in upload mode. | ||
| 80 | 2. Add support for arduino list variable blocks. | ||
| 81 | |||
| 82 | - **Fix bug** | ||
| 83 | |||
| 84 | 1. NodeMCU upload program failed, report: "Not a valid FQBN: not an FQBN". | ||
| 85 | |||
| 86 | ## v2.3.2 | ||
| 87 | |||
| 88 | - **New feature** | ||
| 89 | |||
| 90 | 1. Add support for Arduino K210. | ||
| 91 | 2. Adjust the menu bar layout. | ||
| 92 | 3. Add support for setting the maximum and minimum values of the angle plate, the excess part will be displayed in gray. | ||
| 93 | 4. The color of the numerical slider track changes from gray to change with the color of the parent block. | ||
| 94 | |||
| 95 | - **Fix bug** | ||
| 96 | |||
| 97 | 1. The color of the angle plate does not change with the color of the blocks. | ||
| 98 | 2. Arduino esp8266 compilation and download failed. | ||
| 99 | 3. When generating arduino code, double quote code in string is not escaped | ||
| 100 | |||
| 101 | ## v2.3.1 | ||
| 102 | |||
| 103 | - **New feature** | ||
| 104 | |||
| 105 | 1. The user can use the enter key to send data in the serialport console. | ||
| 106 | 2. Increase default upload baudrate for esp32/8266 to increase upload speed. | ||
| 107 | 3. Delete the blocks in the sensor directory that are not commonly used by esp32. | ||
| 108 | 4. Added support for Ctrl + A/B/C/D shortcut keys in the serial terminal to better interact with the micrpython repl interface. | ||
| 109 | 5. Remove the arduino mini board that is not used frequently. | ||
| 110 | 6. Modify arduino nano download parameters to use old bootloader and add missing A6 A7 pins. | ||
| 111 | 7. Widen upload window. | ||
| 112 | 8. Block the esp32/8266 pins which are used by internal flash. | ||
| 113 | 9. Add input-pulldown mode of esp32 pin mode. | ||
| 114 | |||
| 115 | - **Fix bug** | ||
| 116 | |||
| 117 | 1. The license file is not packaged in the installation package. | ||
| 118 | 2. Fixed the error that the outer frame and the main body of the microbit terminal block were the same color. | ||
| 119 | 3. Fix the error that some int type input can be set to decimal. | ||
| 120 | 4. Microbit show piexl at xx with brightness xx block's brightness parameter don't take effect. | ||
| 121 | 5. Fix the error that the microbit v2 download program fails. | ||
| 122 | |||
| 123 | ## v2.3.0 | ||
| 124 | |||
| 125 | - **New feature** | ||
| 126 | |||
| 127 | 1. Application auto-update feature is now supported. | ||
| 128 | 2. Supports opening multiple apps at the same time. | ||
| 129 | 3. Added Traditional Chinese translation. | ||
| 130 | 4. Add software loading interface. | ||
| 131 | 5. Added code editing support, you can now edit the code after unlocking the code area. | ||
| 132 | 6. Hide sprites and sounds tabs in upload mode. | ||
| 133 | 7. Disable the edit button in the menu bar in upload mode. | ||
| 134 | 8. In the upload mode of micropython, the building blocks of custom list variables can generate code. | ||
| 135 | 9. Optimize and reduce the file size of external resources. | ||
| 136 | 10. Merge the installation files for the 32-bit and 64-bit versions of the windows version. | ||
| 137 | 11. When saving a project without a hardware device, convert it to a format supported by scratch3, so that scratch3 can open the pure scratch project created by openblock. (The save format is still .ob but scratch can be forced to open) | ||
| 138 | |||
| 139 | - **Fix bug** | ||
| 140 | |||
| 141 | 1. Fix the problem that the software needs to copy the cache when it is first started, resulting in no display for a long time. After the user clicks the startup icon multiple times, multiple programs operate on the cache at the same time, causing the cache file to be damaged. Then the program fails to start. | ||
| 142 | 2. Fix wrong translation of button to turn on and off acceleration mode. | ||
| 143 | 3. After selecting arduino uno and then mega2560, the pin menu is not updated. | ||
| 144 | 4. The arduino pin interrupt function code is not right. | ||
| 145 | 5. The python variable increase block will cut off the first digit after inputting more than two digits. | ||
| 146 | 6. The generated code logic is incorrect when using a repeating block on a head block other than a hardware device startup event block. | ||
| 147 | 7. The code of arduino contains block should be 'indexOf' but not 'indexof'. | ||
| 148 | 8. The arduino's comparison block generates code that does not conform to the rules of the C language when the input is a pair of strings or a single character. | ||
| 149 | 9. In micropython, since there is no global declaration for the custom variable in the function defined by the custom function or event, an error will be reported when using the custom variable block under these blocks. | ||
| 150 | 10. Adjust the micropython code generation structure to prevent variables and functions from being called before the definition declaration. | ||
| 151 | 11. Blocks generate code when they are dragged from the toolbar but not yet placed in the workspace. | ||
| 152 | 12. Fix the arduino device sometimes wait for more than ten seconds to start uploading after the compilation is completed. | ||
| 153 | |||
| 154 | ## v2.2.9 | ||
| 155 | |||
| 156 | - **New feature** | ||
| 157 | |||
| 158 | 1. Optimize the windows nsis installation script. Now the first installation path will be set to the root directory of the c drive, and the subsequent installation path will be automatically detected and modified to the installation directory selected during the first installation. | ||
| 159 | 2. Add support for original scratch project files. | ||
| 160 | 3. Add the devil bird to the sprite and custom library. | ||
| 161 | |||
| 162 | - **Fix bug** | ||
| 163 | |||
| 164 | 1. Switching the programming mode while the sprite is speaking will cause the interface to crash. | ||
| 165 | 2. When loading a project file containing custom list variables, an error will be reported that the loading cannot be completed. | ||
| 166 | 3. The parameter blocks of custom functions will be disabled when switching modes. | ||
| 167 | 4. The serial port data of esp32 and microbit is not displayed in the terminal. | ||
| 168 | 5. Correct the programming language icon of esp32/8266. | ||
| 169 | 6. Correct the center coordinates of the demon bird's rotation. | ||
| 170 | |||
| 171 | ## v2.2.8 | ||
| 172 | |||
| 173 | - **New feature** | ||
| 174 | |||
| 175 | 1. Add default program mode setting in the device configuration. | ||
| 176 | |||
| 177 | - **Fix bug** | ||
| 178 | |||
| 179 | 1. After loading a new project file, the connection of the old device is not disconnected. | ||
| 180 | 2. After loading a new project file, if the project does not have a device extension, the code area will be empty. | ||
| 181 | 3. When connecting to a device without fimata service, there will be no alert prompt to download the firmware. | ||
| 182 | 4. Optimize the firmata communication architecture in real-time mode to fix some potential problems. | ||
| 183 | 5. Fix the misspelled device configuration name from leanMore to learnMore. | ||
| 184 | 6. Fix the inaccurate meaning of Arduino block name from whole number to integer. | ||
| 185 | |||
| 186 | ## v2.2.7 | ||
| 187 | |||
| 188 | - **Fix bug** | ||
| 189 | |||
| 190 | 1. After loading the project file, save the project file again and load it, there will be an error and the project cannot be loaded. | ||
| 191 | 2. After loading the project file, click the new project button, and the interface will crash. | ||
| 192 | 3. When opening the extension interface in upload mode, it will freeze for 1~2 seconds before the extension options are loaded. | ||
| 193 | |||
| 194 | ## v2.2.6 | ||
| 195 | |||
| 196 | - **Fix bug** | ||
| 197 | |||
| 198 | 1. The newline parameter of arduino mega2560 serial send block does not take effect. | ||
| 199 | 2. After creating a new sprite, the device selection is cleared. | ||
| 200 | 3. An error occurs after load a project file that contains multiple device extensions. | ||
| 201 | |||
| 202 | ## v2.2.5 | ||
| 203 | |||
| 204 | - **Fix bug** | ||
| 205 | |||
| 206 | 1. Because shield in openblock-resource source code is misspelled as sheild, shield filter in GUI interface is null. | ||
| 207 | 2. In VM, one more line of startheartbeat function call is written, and startheartbeat repeats reentry, resulting in real-time communication error. | ||
| 208 | 3. Add rtscts flow control configuration to repair the situation that some three-party compatible boards cannot be used when opening rtscts flow control. | ||
| 209 | 4. Cannot edit input-box after the alert or confirm window pops up. | ||
| 210 | 5. The device selection is not cleared after a new project is created. | ||
| 211 | 6. The old device is not disconnected after a new project is created. | ||
| 212 | |||
| 213 | ## v2.2.4 | ||
| 214 | |||
| 215 | - **Fix bug** | ||
| 216 | |||
| 217 | 1. There is no A0 ~ A5 option for the read digital pin blocks of control boards such as Arduino UNO. | ||
| 218 | 2. After using the shortcut key Ctrl + Z to modify blocks, the code on the right side is not updated. | ||
| 219 | |||
| 220 | ## v2.2.3 | ||
| 221 | |||
| 222 | - **Fix bug** | ||
| 223 | |||
| 224 | 1. When you double-click to open the project file with the selected device, an error will occur in loading. | ||
| 225 | 2. After add comment for device extension block and save the project file. If try to load the project after restart the software, there is another comment window appear which cannot able to delete. | ||
| 226 | |||
| 227 | ## v2.2.2 | ||
| 228 | |||
| 229 | - **New feature** | ||
| 230 | |||
| 231 | 1. Optimize the font and line break display effect of the serial terminal. | ||
| 232 | 2. Display the loaded extensions first in the extensions library. | ||
| 233 | 3. Modify the default serial port configuration of esp8266 to the official default 76800. | ||
| 234 | 4. Modify the esp8266 upload rate to 921600 to speed up the upload speed. | ||
| 235 | |||
| 236 | - **Fix bug** | ||
| 237 | |||
| 238 | 1. When loading a project with a extension, an error will be reported and cannot be loaded. | ||
| 239 | 2. The input box of the variable increase block is parsed incorrectly when other blocks or variables are placed. | ||
| 240 | 3. In the even sprite, the movement blocks in the toolbox area will not automatically change to the coordinates of the character's current position. | ||
| 241 | 4. Fix the problem that esp32 and esp8266 cannot start after clicking the reset button when connecting to openblock due to the lack of serial port to enable dtr rts flow control. | ||
| 242 | 5. After connecting and disconnecting the device once in upload mode, no matter what mode is connected to the device again, it will not be able to establish communication with the connection firmata. | ||
| 243 | 6. ESP32 and ESP8266 will get stuck for a long time between compiling and uploading. | ||
| 244 | |||
| 245 | ## v2.2.1 | ||
| 246 | |||
| 247 | - **Fix bug** | ||
| 248 | |||
| 249 | 1. The data sent by the serial port is incorrect. | ||
| 250 | 2. Sometimes the project file cannot be opened by double-click or cannot be loaded due to an error when loading the project. | ||
| 251 | 3. Duplicate loading of projects after connecting devices can cause multiple real-time mode listeners to start causing errors. | ||
| 252 | |||
| 253 | ## v2.2.0 | ||
| 254 | |||
| 255 | - **New feature** | ||
| 256 | 1. Add Kit filter option to device selection. | ||
| 257 | 2. Add the option to cancel the device selection. | ||
| 258 | 3. When loading a project containing unknown devices and plug-ins, the error details will be reported. | ||
| 259 | 4. Update the device picture according to the new picture standard. | ||
| 260 | 5. Automatically obtain the control board pin list in external extensions. | ||
| 261 | 6. Add slider type blocks. | ||
| 262 | 7. Optimized the devil bird svg image. | ||
| 263 | 8. Add back edit menu. | ||
| 264 | |||
| 265 | - **Fix bug** | ||
| 266 | 1. The serial port send button is collapsed in small window mode. | ||
| 267 | 2. Modify the default installation path of the desktop version of windows to the root directory of C drive instead of the deep directory of user data. | ||
| 268 | 3. If there is an unsupported device id in the external device list, the device model will be empty. | ||
| 269 | 4. Because the vm building block adds the device type in front of the optype, the display variable cannot be translated normally. | ||
| 270 | 5. Esp8266 digital pin cannot select GPIO16. | ||
| 271 | 6. Check the checkbox so that the variable will be displayed in the stage area, and it will still exist after switching the device. | ||
| 272 | 7. Arduino ceil function name error. | ||
| 273 | 8. The microbit attitude option is not translated. | ||
| 274 | 9. Microbit uses multiple while true statements that are not supported. | ||
| 275 | 10. When using the scroll wheel to move the toolbox, the completely displayed blocks beyond the boundary are blocked again. | ||
| 276 | 11. Color picker function is not available. | ||
| 277 | 12. The disconnection error alert flashes after switching the mode. | ||
| 278 | 13. When using a third-party device, the alert uses the mother board instead of the picture of the third-party device board. | ||
| 279 | 14. Error when load device in no target but has variable. | ||
| 280 | |||
| 281 | ## v2.1.1 | ||
| 282 | |||
| 283 | - **New feature** | ||
| 284 | 1. Add esp8266 and makey makey support. | ||
| 285 | 2. Add a button to show all connectable device. Prevent users from being unable to connect to the device when using a USB-to-serial chip that is not included. | ||
| 286 | 3. Add file associations for .ob project file. | ||
| 287 | |||
| 288 | - **Fix bug** | ||
| 289 | 1. Severe freeze after switching targets several times. | ||
| 290 | 2. The remote resource update address configuration error caused the program to crash after clicking the Check Update button. | ||
| 291 | 3. The remote resource update address incorrectly uses openblockcc instead of the address in the configuration. | ||
| 292 | 4. When the blocks nested inside the blocks in the toolbox are in the workspace, the internal blocks are erroneously disabled when the disabled state is updated. | ||
| 293 | 5. An error is reported after opening multiple windows: the address is already in use. | ||
| 294 | |||
| 295 | ## v2.1.0 | ||
| 296 | |||
| 297 | - **New feature** | ||
| 298 | 1. Change arduino build tool from arduino-builder to arduino-cli. | ||
| 299 | 2. Add remote upgrade function for external extension and device. | ||
| 300 | 3. Modify the default sprite to Demon Bird. | ||
| 301 | 4. Add arduino esp32 board support. | ||
| 302 | 5. Add microbit V2 board support. | ||
| 303 | 6. Add clear cache button. | ||
| 304 | 7. Add install driver button. | ||
| 305 | 8. Move the real-time mode connection indicator to the stage head. | ||
| 306 | 9. Add localization for desktop alters. | ||
| 307 | 10. Add timeout error in upload modal. If it gets stuck for tens of seconds, it will show timeout error, allow users to click the close button but not stuck forever. | ||
| 308 | 11. Add arduino uno ultra base board to support customized board witch A6 A7 pins. | ||
| 309 | 12. Optimize the external extension and device framwork. | ||
| 310 | 13. Optimize the firmware files structure. | ||
| 311 | 14. Optimize the serialport framwork. Prevent interface freeze caused by receiving high-speed serialport data. | ||
| 312 | 15. Add QDP ROBOT C02 kit(arduino esp32). | ||
| 313 | |||
| 314 | - **Fix bug** | ||
| 315 | 1. Stuck at the upload modal if unplug the usb cable while in arduino build progress. | ||
| 316 | 2. Unplug the usb cable while in arduino upload progress, the gui does not disconnect the device. User could still click the upload button and then will stuck in upload modal. | ||
| 317 | 3. Uploading the program or firmware after connecting and disconnecting the device several times will cause the real-time mode communication bug. | ||
| 318 | 4. After the upload is successful, if user do not close the upload window, unplug the usb cable, it will display upload failure. | ||
| 319 | |||
| 320 | ## v2.0.0 | ||
| 321 | |||
| 322 | - **New feature** | ||
| 323 | 1. Add serilport console. | ||
| 324 | 2. Separate third party device from bundle pack. now support modify third party device without rebuild the project. | ||
| 325 | 3. Optimize the block's disable logic in different programming modes. | ||
| 326 | 4. Now in realtime mode, you can select the realtime mode extension. | ||
| 327 | 5. If a block is not connected into the effective tree. it's setup and define and others code will not generate. | ||
| 328 | 6. Optimize the structure of the code generated by Arduino code generator to make it more consistent with Arduino native code format. | ||
| 329 | 7. The project file will save the current programming mode and automatically switch to the saved mode after loading. | ||
| 330 | 8. In programming mode, blocks can no longer be click executed and glow. | ||
| 331 | 9. After the realtime mode communication is successfully established, there will be no atert. Instead, it will indicate whether the communication is successful by dimming the communication icon on the right side of the connection icon. A alter will pop up and prompt to download the real-time mode firmware only after the communication attempt fails. | ||
| 332 | 10. After the firmware is downloaded and the real-time communication is established successfully, the alert of real-time mode failure warning will automatically disappear. | ||
| 333 | |||
| 334 | - **Fix bug** | ||
| 335 | 1. When loading a project file with multiple large extensions, the toolbar area will repeatedly display the contents of multiple extensions, and some other errors. | ||
| 336 | 2. The button to download firmware is not disabled when there is no connected device. | ||
| 337 | 3. Shorten the window of upload to fix the problem of incomplete display on some pc. | ||
| 338 | 4. Fixed a number of potential problems with realtime mode communication. | ||
| 339 | 5. The code window is not re rendered after resizing, resulting in a missing display. | ||
| 340 | 6. After switching programming mode, the sprite will disappear in some times. | ||
| 341 | 7. Arduino and microbit do not hide all the unsupported building blocks in programming mode. | ||
| 342 | 8. Microbit's buttonIsPressed block transcoding function should have written n for b when button is B. | ||
| 343 | 9. Microbit custom variable name exception. | ||
| 344 | 10. Arduino UNO mega2560 serial port 0 translation code is incorrect. | ||
| 345 | 11. Number parsing error of data_changevariableby block. | ||
| 346 | 12. Cancel the 1.05x interface zoom setting and directly enlarge the font to fix the problem of blurred font in the toolbar menu. | ||
| 347 | |||
| 348 | ## v1.2.2beta | ||
| 349 | |||
| 350 | - **New feature** | ||
| 351 | 1. Add hide code stage button. | ||
| 352 | 2. Change the ui of upload button. | ||
| 353 | 3. Change the description of some boards. | ||
| 354 | 4. Add a 1.05 scale to fix the problem of fuzzy font. | ||
| 355 | 5. Change project file extension from .sb3 to .ob. | ||
| 356 | |||
| 357 | - **Fix bug** | ||
| 358 | 1. Microbit generator error. | ||
| 359 | 2. The pin menu of arduino set digital out does not have analog pin items. | ||
| 360 | |||
| 361 | ## v1.2.1beta | ||
| 362 | |||
| 363 | - **Fix bug** | ||
| 364 | |||
| 365 | 1. Third party's block which from vm code generator error. | ||
| 366 | |||
| 367 | ## v1.2.0beta | ||
| 368 | |||
| 369 | - **New feature** | ||
| 370 | |||
| 371 | 1. Now most alert will automaticly disapear after 5s. | ||
| 372 | 2. Completed the blocks of microbit. | ||
| 373 | 3. Add a servo extension as demo for microbit. | ||
| 374 | 4. After installing the new version of the software, the old cache will be cleared automatically. | ||
| 375 | |||
| 376 | - **New device/kit** | ||
| 377 | |||
| 378 | 1. Arduino Mini | ||
| 379 | 2. QDP Robot(齐护机器人) kit | ||
| 380 | |||
| 381 | - **Fix bug** | ||
| 382 | |||
| 383 | 1. Error usb hardware id of cp2102. | ||
| 384 | 2. Error translation of microbit. | ||
| 385 | 3. Cannot scan to devices after loading a project. | ||
| 386 | 4. The loaded device extension still exists after switching the device selection. | ||
| 387 | |||
| 388 | ## v1.1.0beta | ||
| 389 | |||
| 390 | - **New feature** | ||
| 391 | |||
| 392 | 1. Blocks could over flow the flyout boundary when mouse enter. | ||
| 393 | 2. Extension can be auto loaded when device selected. | ||
| 394 | |||
| 395 | - **New device/kit** | ||
| 396 | |||
| 397 | 1. microbit | ||
| 398 | 2. iron robot kit | ||
| 399 |
LICENSE
0 → 100644
| 1 | MIT License | ||
| 2 | |||
| 3 | Copyright (c) 2021 OpenBlock.cc | ||
| 4 | |||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | of this software and associated documentation files (the "Software"), to deal | ||
| 7 | in the Software without restriction, including without limitation the rights | ||
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | copies of the Software, and to permit persons to whom the Software is | ||
| 10 | furnished to do so, subject to the following conditions: | ||
| 11 | |||
| 12 | The above copyright notice and this permission notice shall be included in all | ||
| 13 | copies or substantial portions of the Software. | ||
| 14 | |||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | SOFTWARE. | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
LICENSE.ScratchFoundation
0 → 100644
| 1 | Copyright (c) 2019, Scratch Foundation | ||
| 2 | All rights reserved. | ||
| 3 | |||
| 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
| 5 | |||
| 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
| 7 | |||
| 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
| 9 | |||
| 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. | ||
| 11 | |||
| 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
README.md
0 → 100644
| 1 | # openblock-desktop | ||
| 2 | |||
| 3 | ## 安装依赖 | ||
| 4 | |||
| 5 | 1. 需要稳定的代理工具 | ||
| 6 | |||
| 7 | 2. ```javascript | ||
| 8 | //代理到工具指定端口 | ||
| 9 | npm config set proxy=http://localhost:xxxx | ||
| 10 | //使用npm源 使用淘宝源 遇到了 包文件hash不一致的情况 | ||
| 11 | npx nrm use npm | ||
| 12 | ``` | ||
| 13 | |||
| 14 | 3. 至少需要python3.12的环境,原因`openblock-link`中的`@abandonware/bluetooth-hci-socket`的依赖库中指定了node-gyp的版本 `10.1.0` | ||
| 15 | |||
| 16 | |||
| 17 | |||
| 18 | |||
| 19 | |||
| 20 |  | ||
| 21 |  | ||
| 22 | |||
| 23 | ## Getting Start | ||
| 24 | |||
| 25 | Visit the wiki: [https://openblockcc.github.io](https://openblockcc.github.io) | ||
| 26 |
TRADEMARK
0 → 100644
| 1 | The Openblock trademarks, including the Openblock name, logo and the Openblock Devil Bird graphics(the "Marks"), are property of the Openblock.cc. Marks may not be used to endorse or promote non-software products products derived from this software without specific prior written permission. | ||
| 2 | |||
| 3 | Without retaining the openblock name and logo, Openblock Devil Bird cannot be used for redistribution and use in source code and binary form without prior express written permission. |
buildResources/.gitignore
0 → 100644
buildResources/OpenBlockDesktop.icns
0 → 100644
No preview for this file type
buildResources/OpenBlockDesktop.ico
0 → 100644
No preview for this file type
buildResources/OpenBlockFile.icns
0 → 100644
No preview for this file type
buildResources/OpenBlockFile.ico
0 → 100644
No preview for this file type
buildResources/appx/Square150x150Logo.png
0 → 100644
19.2 KB
buildResources/appx/Square44x44Logo.png
0 → 100644
15.8 KB
buildResources/appx/StoreLogo.png
0 → 100644
16.1 KB
buildResources/appx/Wide310x150Logo.png
0 → 100644
19.5 KB
buildResources/entitlements.mac.plist
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| 3 | <plist version="1.0"> | ||
| 4 | <dict> | ||
| 5 | <key>com.apple.security.cs.allow-dyld-environment-variables</key> | ||
| 6 | <true/> | ||
| 7 | <key>com.apple.security.cs.allow-jit</key> | ||
| 8 | <true/> | ||
| 9 | <key>com.apple.security.cs.allow-unsigned-executable-memory</key> | ||
| 10 | <true/> | ||
| 11 | <key>com.apple.security.device.audio-input</key> | ||
| 12 | <true/> | ||
| 13 | <key>com.apple.security.device.camera</key> | ||
| 14 | <true/> | ||
| 15 | <key>com.apple.security.device.microphone</key> | ||
| 16 | <true/> | ||
| 17 | <key>com.apple.security.files.user-selected.read-only</key> | ||
| 18 | <true/> | ||
| 19 | <key>com.apple.security.files.user-selected.read-write</key> | ||
| 20 | <true/> | ||
| 21 | <key>com.apple.security.network.client</key> | ||
| 22 | <true/> | ||
| 23 | </dict> | ||
| 24 | </plist> |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| 3 | <plist version="1.0"> | ||
| 4 | <dict> | ||
| 5 | <key>com.apple.security.app-sandbox</key> | ||
| 6 | <true/> | ||
| 7 | <key>com.apple.security.inherit</key> | ||
| 8 | <true/> | ||
| 9 | </dict> | ||
| 10 | </plist> |
buildResources/entitlements.mas.plist
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| 3 | <plist version="1.0"> | ||
| 4 | <dict> | ||
| 5 | <key>com.apple.security.app-sandbox</key> | ||
| 6 | <true/> | ||
| 7 | <key>com.apple.security.cs.allow-dyld-environment-variables</key> | ||
| 8 | <true/> | ||
| 9 | <key>com.apple.security.cs.allow-jit</key> | ||
| 10 | <true/> | ||
| 11 | <key>com.apple.security.cs.allow-unsigned-executable-memory</key> | ||
| 12 | <true/> | ||
| 13 | <key>com.apple.security.device.audio-input</key> | ||
| 14 | <true/> | ||
| 15 | <key>com.apple.security.device.camera</key> | ||
| 16 | <true/> | ||
| 17 | <key>com.apple.security.device.microphone</key> | ||
| 18 | <true/> | ||
| 19 | <key>com.apple.security.files.user-selected.read-only</key> | ||
| 20 | <true/> | ||
| 21 | <key>com.apple.security.files.user-selected.read-write</key> | ||
| 22 | <true/> | ||
| 23 | <key>com.apple.security.network.client</key> | ||
| 24 | <true/> | ||
| 25 | </dict> | ||
| 26 | </plist> |
buildResources/installer.nsh
0 → 100644
| 1 | !include x64.nsh | ||
| 2 | !include LogicLib.nsh | ||
| 3 | !include StrFunc.nsh | ||
| 4 | ${StrRep} | ||
| 5 | |||
| 6 | !macro preInit | ||
| 7 | |||
| 8 | ${If} ${RunningX64} | ||
| 9 | SetRegView 64 | ||
| 10 | ${EndIf} | ||
| 11 | |||
| 12 | ${StrRep} $0 "${UNINSTALL_REGISTRY_KEY}" "Software" "SOFTWARE" | ||
| 13 | ${StrRep} $1 "${INSTALL_REGISTRY_KEY}" "Software" "SOFTWARE" | ||
| 14 | |||
| 15 | ReadRegStr $R0 HKCU "$0" "UninstallString" | ||
| 16 | ReadRegStr $R1 HKCU "$1" "InstallLocation" | ||
| 17 | |||
| 18 | StrCmp $R0 "" 0 +4 | ||
| 19 | |||
| 20 | ReadRegStr $R0 HKLM "$0" "UninstallString" | ||
| 21 | ReadRegStr $R1 HKLM "$1" "InstallLocation" | ||
| 22 | |||
| 23 | StrCmp $R0 "" 0 done | ||
| 24 | StrCmp $R1 "" 0 done | ||
| 25 | |||
| 26 | WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\OpenBlockDesktop" | ||
| 27 | WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\OpenBlockDesktop" | ||
| 28 | |||
| 29 | done: | ||
| 30 | ${If} ${RunningX64} | ||
| 31 | SetRegView LastUsed | ||
| 32 | ${EndIf} | ||
| 33 | |||
| 34 | !macroend | ||
| 35 | |||
| 36 | !macro customUnInstall | ||
| 37 | |||
| 38 | ${If} ${RunningX64} | ||
| 39 | SetRegView 64 | ||
| 40 | ${EndIf} | ||
| 41 | |||
| 42 | DeleteRegKey HKLM "${INSTALL_REGISTRY_KEY}" | ||
| 43 | DeleteRegKey HKCU "${INSTALL_REGISTRY_KEY}" | ||
| 44 | |||
| 45 | ${If} ${RunningX64} | ||
| 46 | SetRegView LastUsed | ||
| 47 | ${EndIf} | ||
| 48 | |||
| 49 | !macroend |
buildResources/linux/512x512.png
0 → 100644
32.5 KB
buildResources/make-icons.sh
0 → 100755
| 1 | #!/bin/bash | ||
| 2 | SRC=../src/icon/OpenBlockDesktop.svg | ||
| 3 | OUT_ICONSET=OpenBlockDesktop.iconset | ||
| 4 | OUT_ICNS=OpenBlockDesktop.icns | ||
| 5 | OUT_ICO=OpenBlockDesktop.ico | ||
| 6 | TMP_ICO=tmp | ||
| 7 | |||
| 8 | ICO_BASIC_SIZES="16 24 32 48 256" | ||
| 9 | ICO_EXTRA_SIZES="20 30 36 40 60 64 72 80 96 512" | ||
| 10 | |||
| 11 | if command -v pngcrush >/dev/null 2>&1; then | ||
| 12 | function optimize () { | ||
| 13 | pngcrush -new -brute -ow "$@" | ||
| 14 | } | ||
| 15 | else | ||
| 16 | echo "pngcrush is not available - skipping PNG optimization" | ||
| 17 | function optimize () { | ||
| 18 | echo "Not optimizing:" "$@" | ||
| 19 | } | ||
| 20 | fi | ||
| 21 | |||
| 22 | # usage: resize newWidth newHeight input output [otherOptions...] | ||
| 23 | function resize () { | ||
| 24 | WIDTH=$1 | ||
| 25 | HEIGHT=$2 | ||
| 26 | SRC=$3 | ||
| 27 | DST=$4 | ||
| 28 | shift 4 | ||
| 29 | convert -background none -resize "${WIDTH}x${HEIGHT}" -extent "${WIDTH}x${HEIGHT}" -gravity center "$@" "${SRC}" "${DST}" | ||
| 30 | optimize "${DST}" | ||
| 31 | } | ||
| 32 | |||
| 33 | if command -v convert >/dev/null 2>&1; then | ||
| 34 | # Mac | ||
| 35 | if command -v iconutil >/dev/null 2>&1; then | ||
| 36 | mkdir -p "${OUT_ICONSET}" | ||
| 37 | for SIZE in 16 32 128 256 512; do | ||
| 38 | SIZE2=`expr "${SIZE}" '*' 2` | ||
| 39 | resize "${SIZE}" "${SIZE}" "${SRC}" "${OUT_ICONSET}/icon_${SIZE}x${SIZE}.png" -density 72 -units PixelsPerInch | ||
| 40 | resize "${SIZE2}" "${SIZE2}" "${SRC}" "${OUT_ICONSET}/icon_${SIZE}x${SIZE}@2x.png" -density 144 -units PixelsPerInch | ||
| 41 | done | ||
| 42 | iconutil -c icns --output "${OUT_ICNS}" "${OUT_ICONSET}" | ||
| 43 | else | ||
| 44 | echo "iconutil is not available - skipping ICNS and ICONSET" | ||
| 45 | fi | ||
| 46 | |||
| 47 | # Windows ICO | ||
| 48 | mkdir -p "${TMP_ICO}" | ||
| 49 | for SIZE in ${ICO_BASIC_SIZES} ${ICO_EXTRA_SIZES}; do | ||
| 50 | resize "${SIZE}" "${SIZE}" "${SRC}" "${TMP_ICO}/icon_${SIZE}x${SIZE}.png" | ||
| 51 | done | ||
| 52 | # Asking for "Zip" compression actually results in PNG compression | ||
| 53 | convert "${TMP_ICO}"/icon_*.png -colorspace sRGB -compress Zip "${OUT_ICO}" | ||
| 54 | |||
| 55 | # Windows AppX | ||
| 56 | mkdir -p "appx" | ||
| 57 | resize 44 44 "${SRC}" 'appx/Square44x44Logo.png' | ||
| 58 | resize 50 50 "${SRC}" 'appx/StoreLogo.png' | ||
| 59 | resize 150 150 "${SRC}" 'appx/Square150x150Logo.png' | ||
| 60 | resize 310 150 "${SRC}" 'appx/Wide310x150Logo.png' | ||
| 61 | else | ||
| 62 | echo "ImageMagick is not available - cannot convert icons" | ||
| 63 | fi |
buildResources/screenshot.png
0 → 100644
75.7 KB
doc/alipayQRCode.png
0 → 100644
47.3 KB
doc/screenshot.png
0 → 100644
231 KB
doc/screenshot2.png
0 → 100644
427 KB
electron-builder.yaml
0 → 100644
| 1 | directories: | ||
| 2 | buildResources: buildResources | ||
| 3 | output: dist | ||
| 4 | extraFiles: ['LICENSE', 'LICENSE.ScratchFoundation', 'TRADEMARK', "tools", "external-resources", 'firmwares', "drivers"] | ||
| 5 | |||
| 6 | appId: openblock.cc.openblock-desktop | ||
| 7 | productName: "OpenBlockDesktop" | ||
| 8 | publish: | ||
| 9 | - provider: github | ||
| 10 | artifactName: "OpenBlock-Desktop_v${version}_${os}_${arch}.${ext}" | ||
| 11 | |||
| 12 | fileAssociations: | ||
| 13 | ext: ob | ||
| 14 | name: OpenBlock project file | ||
| 15 | role: Editor | ||
| 16 | icon: buildResources/OpenBlockFile.ico | ||
| 17 | |||
| 18 | mac: | ||
| 19 | category: public.app-category.education | ||
| 20 | entitlements: buildResources/entitlements.mac.plist | ||
| 21 | extendInfo: | ||
| 22 | NSCameraUsageDescription: >- | ||
| 23 | This app requires camera access when using the video sensing blocks. | ||
| 24 | NSMicrophoneUsageDescription: >- | ||
| 25 | This app requires microphone access when recording sounds or detecting loudness. | ||
| 26 | gatekeeperAssess: true | ||
| 27 | hardenedRuntime: true | ||
| 28 | icon: buildResources/OpenBlockDesktop.icns | ||
| 29 | provisioningProfile: embedded.provisionprofile | ||
| 30 | target: | ||
| 31 | - dmg | ||
| 32 | # - mas | ||
| 33 | dmg: | ||
| 34 | title: "OpenBlock-Desktop_${version}" | ||
| 35 | # mas: | ||
| 36 | # category: public.app-category.education | ||
| 37 | # entitlements: buildResources/entitlements.mas.plist | ||
| 38 | # entitlementsInherit: buildResources/entitlements.mas.inherit.plist | ||
| 39 | # hardenedRuntime: false | ||
| 40 | # icon: buildResources/OpenBlockDesktop.icns | ||
| 41 | # masDev: | ||
| 42 | # type: development | ||
| 43 | # provisioningProfile: mas-dev.provisionprofile | ||
| 44 | |||
| 45 | win: | ||
| 46 | icon: buildResources/OpenBlockDesktop.ico | ||
| 47 | target: | ||
| 48 | # - appx | ||
| 49 | - nsis | ||
| 50 | # appx: | ||
| 51 | # identityName: "OpenblockTeam.OpenblockDesktop | ||
| 52 | # publisherDisplayName: "OpenBlock Team" | ||
| 53 | # publisher: "CN=2EC43DF1-469A-4119-9AB9-568A0A1FF65F" | ||
| 54 | nsis: | ||
| 55 | oneClick: false # allow user to choose per-user or per-machine | ||
| 56 | allowToChangeInstallationDirectory: true | ||
| 57 | include: buildResources/installer.nsh | ||
| 58 | # license: LICENSE | ||
| 59 | |||
| 60 | linux: | ||
| 61 | category: Education | ||
| 62 | icon: buildResources/linux | ||
| 63 | desktop: | ||
| 64 | - Encoding: UTF-8 | ||
| 65 | - Name: OpenBlockDesktop | ||
| 66 | - Icon: openblock-desktop | ||
| 67 | - Type: Application | ||
| 68 | - Terminal: false | ||
| 69 | target: | ||
| 70 | - deb | ||
| 71 | |||
| 72 | deb: | ||
| 73 | depends: ["libnotify4", "libxtst6", "libnss3"] | ||
| 74 |
electron-webpack.json5
0 → 100644
package-lock.json
0 → 100644
This diff could not be displayed because it is too large.
package.json
0 → 100644
| 1 | { | ||
| 2 | "name": "openblock-desktop", | ||
| 3 | "productName": "OpenBlockDesktop", | ||
| 4 | "description": "OpenBlock as a self-contained desktop application", | ||
| 5 | "author": "Openblock.cc Team <contact@openblock.cc> (http://www.openblock.cc/)", | ||
| 6 | "version": "2.5.2", | ||
| 7 | "license": "MIT", | ||
| 8 | "scripts": { | ||
| 9 | "postinstall": "npx electron-builder install-app-deps", | ||
| 10 | "clean": "rimraf ./dist ./static ./external-resources ./tools ./translations ./firmwares ./drivers", | ||
| 11 | "i18n:src": "mkdirp translations/core && format-message extract --out-file translations/core/en.json src/main/**.js", | ||
| 12 | "i18n:push": "tx-push-src openblock-editor desktop translations/core/en.json", | ||
| 13 | "start": "mkdirp ./dist && electron-webpack dev --bail --display-error-details --env.minify=false --no-progress", | ||
| 14 | "compile": "mkdirp ./dist && electron-webpack --bail --display-error-details --env.minify=false --no-progress", | ||
| 15 | "fetch:drivers": "rimraf ./drivers && node scripts/download-driver.js", | ||
| 16 | "fetch:exts": "rimraf ./external-resources && node ./node_modules/openblock-resource/script/download.js --repo=openblockcc/external-resources-v3", | ||
| 17 | "fetch:firmwares": "rimraf ./firmwares && node ./node_modules/openblock-link/script/download-firmwares.js", | ||
| 18 | "fetch:tools": "rimraf ./tools && node ./node_modules/openblock-link/script/download-tools.js", | ||
| 19 | "fetch:static": "rimraf ./static && mkdirp ./static && git clone https://github.com/openblockcc/openblock-assets.git static && rimraf ./static/.git", | ||
| 20 | "fetch:all": "npm run fetch:drivers && npm run fetch:exts && npm run fetch:firmwares && npm run fetch:tools && npm run fetch:static", | ||
| 21 | "build": "npm run build:dev", | ||
| 22 | "build:dev": "npm run compile && npm run doBuild -- --mode=dev", | ||
| 23 | "build:dir": "npm run compile && npm run doBuild -- --mode=dir", | ||
| 24 | "build:dist": "npm run compile && npm run doBuild -- --mode=dist", | ||
| 25 | "build:publish": "npm run compile && npm run doBuild -- --mode=publish", | ||
| 26 | "doBuild": "node ./scripts/electron-builder-wrapper.js", | ||
| 27 | "dist": "npm run clean && npm run compile && npm run fetch:all && npm run doBuild -- --mode=dist", | ||
| 28 | "publish": "npm run clean && npm run compile && npm run fetch:all && npm run doBuild -- --mode=publish", | ||
| 29 | "test": "npm run test:lint", | ||
| 30 | "test:lint": "eslint --cache --color --ext .jsx,.js src scripts" | ||
| 31 | }, | ||
| 32 | "repository": { | ||
| 33 | "type": "git", | ||
| 34 | "url": "git+ssh://git@github.com/openblockcc/openblock-desktop.git" | ||
| 35 | }, | ||
| 36 | "dependencies": { | ||
| 37 | "bytes": "^3.1.2", | ||
| 38 | "openblock-link": "^0.2.0-prerelease.20240710015813", | ||
| 39 | "openblock-resource": "^0.2.0-prerelease.20240708150142", | ||
| 40 | "postinstall": "^0.7.4", | ||
| 41 | "source-map-support": "^0.5.19" | ||
| 42 | }, | ||
| 43 | "devDependencies": { | ||
| 44 | "@aws-sdk/client-s3": "^3.53.1", | ||
| 45 | "@babel/core": "^7.9.6", | ||
| 46 | "@babel/plugin-proposal-object-rest-spread": "^7.9.6", | ||
| 47 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", | ||
| 48 | "@babel/plugin-transform-async-to-generator": "^7.8.3", | ||
| 49 | "@babel/preset-env": "^7.9.6", | ||
| 50 | "@babel/preset-react": "^7.9.4", | ||
| 51 | "@electron/remote": "^2.0.5", | ||
| 52 | "async": "^3.2.0", | ||
| 53 | "babel-eslint": "^10.1.0", | ||
| 54 | "babel-loader": "^8.1.0", | ||
| 55 | "babel-plugin-react-intl": "^7.5.7", | ||
| 56 | "classnames": "^2.3.1", | ||
| 57 | "copy-webpack-plugin": "^5.1.1", | ||
| 58 | "download-github-release": "^0.3.2", | ||
| 59 | "electron": "^15.3.1", | ||
| 60 | "electron-builder": "^22.14.13", | ||
| 61 | "electron-devtools-installer": "^3.2.0", | ||
| 62 | "electron-fetch": "^1.7.4", | ||
| 63 | "electron-log": "^4.4.6", | ||
| 64 | "electron-notarize": "^1.1.1", | ||
| 65 | "electron-store": "^8.0.1", | ||
| 66 | "electron-updater": "^4.6.5", | ||
| 67 | "electron-webpack": "^2.8.2", | ||
| 68 | "eslint": "^7.0.0", | ||
| 69 | "eslint-config-scratch": "^6.0.0", | ||
| 70 | "eslint-plugin-import": "^2.29.1", | ||
| 71 | "eslint-plugin-react": "^7.20.0", | ||
| 72 | "format-message": "^6.2.3", | ||
| 73 | "format-message-cli": "^6.2.3", | ||
| 74 | "fs-extra": "^11.1.0", | ||
| 75 | "intl": "1.2.5", | ||
| 76 | "lodash.bindall": "^4.4.0", | ||
| 77 | "lodash.defaultsdeep": "^4.6.1", | ||
| 78 | "minilog": "^3.1.0", | ||
| 79 | "minimist": "^1.2.5", | ||
| 80 | "mkdirp": "^1.0.4", | ||
| 81 | "monaco-editor-webpack-plugin": "^1.7.0", | ||
| 82 | "nets": "^3.2.0", | ||
| 83 | "node-abort-controller": "^3.0.1", | ||
| 84 | "openblock-gui": "github:openblockcc/openblock-gui#openblock-desktop-v2.5.2", | ||
| 85 | "openblock-l10n": "^3.15.20240704111820", | ||
| 86 | "openblock-parse-release-message": "0.0.4", | ||
| 87 | "os-locale": "^5.0.0", | ||
| 88 | "react": "^16.14.0", | ||
| 89 | "react-dom": "^16.14.0", | ||
| 90 | "react-intl": "2.9.0", | ||
| 91 | "react-redux": "5.0.7", | ||
| 92 | "redux": "3.7.2", | ||
| 93 | "rimraf": "^3.0.2", | ||
| 94 | "sudo-prompt": "^9.2.1", | ||
| 95 | "uuid": "^8.3.2", | ||
| 96 | "webpack": "^4.43.0" | ||
| 97 | } | ||
| 98 | } |
scripts/afterSign.js
0 → 100644
| 1 | const {notarize} = require('electron-notarize'); | ||
| 2 | |||
| 3 | const notarizeMacBuild = async function (context) { | ||
| 4 | // keep this in sync with appId in the electron-builder config | ||
| 5 | const appId = 'edu.mit.scratch.scratch-desktop'; | ||
| 6 | |||
| 7 | if (!process.env.AC_USERNAME) { | ||
| 8 | console.error([ | ||
| 9 | 'This build is not notarized and will not run on newer versions of macOS!', | ||
| 10 | 'Notarizing the macOS build requires an Apple ID. To notarize future builds:', | ||
| 11 | '* Set the environment variable AC_USERNAME to your@apple.id and', | ||
| 12 | '* Either set AC_PASSWORD or ensure your keychain has an item for "Application Loader: your@apple.id"' | ||
| 13 | ].join('\n')); | ||
| 14 | return; | ||
| 15 | } | ||
| 16 | |||
| 17 | const appleId = process.env.AC_USERNAME; | ||
| 18 | const appleIdKeychainItem = `Application Loader: ${appleId}`; | ||
| 19 | |||
| 20 | if (process.env.AC_PASSWORD) { | ||
| 21 | console.log(`Notarizing with Apple ID "${appleId}" and a password`); | ||
| 22 | } else { | ||
| 23 | console.log(`Notarizing with Apple ID "${appleId}" and keychain item "${appleIdKeychainItem}"`); | ||
| 24 | } | ||
| 25 | |||
| 26 | const {appOutDir} = context; | ||
| 27 | const productFilename = context.packager.appInfo.productFilename; | ||
| 28 | await notarize({ | ||
| 29 | appBundleId: appId, | ||
| 30 | appPath: `${appOutDir}/${productFilename}.app`, | ||
| 31 | appleId, | ||
| 32 | appleIdPassword: process.env.AC_PASSWORD || `@keychain:${appleIdKeychainItem}` | ||
| 33 | }); | ||
| 34 | }; | ||
| 35 | |||
| 36 | const afterSign = async function (context) { | ||
| 37 | const {electronPlatformName} = context; | ||
| 38 | |||
| 39 | switch (electronPlatformName) { | ||
| 40 | case 'mas': // macOS build for Mac App Store | ||
| 41 | break; | ||
| 42 | case 'darwin': // macOS build NOT for Mac App Store | ||
| 43 | await notarizeMacBuild(context); | ||
| 44 | break; | ||
| 45 | } | ||
| 46 | }; | ||
| 47 | |||
| 48 | module.exports = afterSign; |
scripts/download-driver.js
0 → 100644
| 1 | /* eslint-disable */ | ||
| 2 | const downloadRelease = require('download-github-release'); | ||
| 3 | const path = require('path'); | ||
| 4 | const os = require('os'); | ||
| 5 | const fs = require('fs'); | ||
| 6 | |||
| 7 | const user = 'openblockcc'; | ||
| 8 | const repo = 'openblock-driver'; | ||
| 9 | const outputdir = path.join(__dirname, '../drivers'); | ||
| 10 | const leaveZipped = false; | ||
| 11 | |||
| 12 | function filterRelease (release) { | ||
| 13 | return release.prerelease === false; | ||
| 14 | } | ||
| 15 | |||
| 16 | function filterAsset(asset) { | ||
| 17 | return (asset.name.indexOf(os.platform()) >= 0); | ||
| 18 | } | ||
| 19 | |||
| 20 | if (!fs.existsSync(outputdir)) { | ||
| 21 | fs.mkdirSync(outputdir, {recursive: true}); | ||
| 22 | } | ||
| 23 | |||
| 24 | downloadRelease(user, repo, outputdir, filterRelease, filterAsset, leaveZipped) | ||
| 25 | .then(() => { | ||
| 26 | console.log('Tools download complete'); | ||
| 27 | }) | ||
| 28 | .catch(err => { | ||
| 29 | console.error(err.message); | ||
| 30 | }); |
scripts/electron-builder-wrapper.js
0 → 100644
| 1 | /** | ||
| 2 | * @overview This script runs `electron-builder` with special management of code signing configuration on Windows. | ||
| 3 | * Running this script with no command line parameters should build all targets for the current platform. | ||
| 4 | * On Windows, make sure to set CSC_* or WIN_CSC_* environment variables or the NSIS build will fail. | ||
| 5 | * On Mac, the CSC_* variables are optional but will be respected if present. | ||
| 6 | * See also: https://www.electron.build/code-signing | ||
| 7 | */ | ||
| 8 | |||
| 9 | const {spawnSync} = require('child_process'); | ||
| 10 | const fs = require('fs'); | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Strip any code signing configuration (CSC) from a set of environment variables. | ||
| 14 | * @param {object} environment - a collection of environment variables which might include code signing configuration. | ||
| 15 | * @returns {object} - a collection of environment variables which does not include code signing configuration. | ||
| 16 | */ | ||
| 17 | const stripCSC = function (environment) { | ||
| 18 | const { | ||
| 19 | CSC_LINK: _CSC_LINK, | ||
| 20 | CSC_KEY_PASSWORD: _CSC_KEY_PASSWORD, | ||
| 21 | WIN_CSC_LINK: _WIN_CSC_LINK, | ||
| 22 | WIN_CSC_KEY_PASSWORD: _WIN_CSC_KEY_PASSWORD, | ||
| 23 | ...strippedEnvironment | ||
| 24 | } = environment; | ||
| 25 | return strippedEnvironment; | ||
| 26 | }; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * @returns {string} - an `electron-builder` flag to build for the current platform, based on `process.platform`. | ||
| 30 | */ | ||
| 31 | const getPlatformFlag = function () { | ||
| 32 | switch (process.platform) { | ||
| 33 | case 'win32': return '--windows'; | ||
| 34 | case 'darwin': return '--macos'; | ||
| 35 | case 'linux': return '--linux'; | ||
| 36 | } | ||
| 37 | throw new Error(`Could not determine platform flag for platform: ${process.platform}`); | ||
| 38 | }; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Run `electron-builder` once to build one or more target(s). | ||
| 42 | * @param {object} wrapperConfig - overall configuration object for the wrapper script. | ||
| 43 | * @param {object} target - the target to build in this call. | ||
| 44 | * If the `target.name` is `'nsis'` then the environment must contain code-signing config (CSC_* or WIN_CSC_*). | ||
| 45 | * If the `target.name` is `'appx'` then code-signing config will be stripped from the environment if present. | ||
| 46 | */ | ||
| 47 | const runBuilder = function (wrapperConfig, target) { | ||
| 48 | // the AppX build fails if CSC_* or WIN_CSC_* variables are set | ||
| 49 | const shouldStripCSC = (target.name.indexOf('appx') === 0) || (!wrapperConfig.doSign); | ||
| 50 | const childEnvironment = shouldStripCSC ? stripCSC(process.env) : process.env; | ||
| 51 | if (wrapperConfig.doSign && | ||
| 52 | (target.name.indexOf('nsis') === 0) && | ||
| 53 | !(childEnvironment.CSC_LINK || childEnvironment.WIN_CSC_LINK)) { | ||
| 54 | // throw new Error(`Signing NSIS build requires CSC_LINK or WIN_CSC_LINK`); | ||
| 55 | } | ||
| 56 | const platformFlag = getPlatformFlag(); | ||
| 57 | let allArgs = [platformFlag, target.name]; | ||
| 58 | if (target.platform === 'darwin') { | ||
| 59 | allArgs.push(`--c.mac.type=${wrapperConfig.mode === 'dist' ? 'distribution' : 'development'}`); | ||
| 60 | if (target.name === 'mas-dev') { | ||
| 61 | allArgs.push('--c.mac.provisioningProfile=mas-dev.provisionprofile'); | ||
| 62 | } | ||
| 63 | if (wrapperConfig.doSign) { | ||
| 64 | // really this is "notarize only if we also sign" | ||
| 65 | allArgs.push('--c.afterSign=scripts/afterSign.js'); | ||
| 66 | } else { | ||
| 67 | allArgs.push('--c.mac.identity=null'); | ||
| 68 | } | ||
| 69 | } | ||
| 70 | if (target.platform === 'win32' && wrapperConfig.mode !== 'dev') { | ||
| 71 | allArgs.push('--ia32', '--x64'); | ||
| 72 | } | ||
| 73 | if (!wrapperConfig.doPackage) { | ||
| 74 | allArgs.push('--dir', '--c.compression=store'); | ||
| 75 | } | ||
| 76 | if (wrapperConfig.doPublish) { | ||
| 77 | allArgs.push('--publish', 'always'); | ||
| 78 | } else { | ||
| 79 | // Prevent electron build from automatically publishing in github action | ||
| 80 | allArgs.push('--publish', 'never'); | ||
| 81 | } | ||
| 82 | allArgs = allArgs.concat(wrapperConfig.builderArgs); | ||
| 83 | console.log(`running electron-builder with arguments: ${allArgs}`); | ||
| 84 | const result = spawnSync('electron-builder', allArgs, { | ||
| 85 | env: childEnvironment, | ||
| 86 | shell: true, | ||
| 87 | stdio: 'inherit' | ||
| 88 | }); | ||
| 89 | if (result.error) { | ||
| 90 | throw result.error; | ||
| 91 | } | ||
| 92 | if (result.signal) { | ||
| 93 | throw new Error(`Child process terminated due to signal ${result.signal}`); | ||
| 94 | } | ||
| 95 | if (result.status) { | ||
| 96 | throw new Error(`Child process returned status code ${result.status}`); | ||
| 97 | } | ||
| 98 | }; | ||
| 99 | |||
| 100 | /** | ||
| 101 | * @param {object} wrapperConfig - overall configuration object for the wrapper script. | ||
| 102 | * @returns {Array.<object>} - the default list of targets on this platform. Each item in the array represents one | ||
| 103 | * call to `runBuilder` for exactly one build target. In theory electron-builder can build two or more targets at the | ||
| 104 | * same time but doing so limits has unwanted side effects on both macOS and Windows (see function body). | ||
| 105 | */ | ||
| 106 | const calculateTargets = function (wrapperConfig) { | ||
| 107 | const masDevProfile = 'mas-dev.provisionprofile'; | ||
| 108 | const availableTargets = { | ||
| 109 | macAppStore: { | ||
| 110 | name: 'mas', | ||
| 111 | platform: 'darwin' | ||
| 112 | }, | ||
| 113 | macAppStoreDev: { | ||
| 114 | name: 'mas-dev', | ||
| 115 | platform: 'darwin' | ||
| 116 | }, | ||
| 117 | macDirectDownload: { | ||
| 118 | name: 'dmg', | ||
| 119 | platform: 'darwin' | ||
| 120 | }, | ||
| 121 | // microsoftStore: { | ||
| 122 | // name: 'appx:ia32 appx:x64', | ||
| 123 | // platform: 'win32' | ||
| 124 | // }, | ||
| 125 | windowsDirectDownload: { | ||
| 126 | name: 'nsis', | ||
| 127 | platform: 'win32' | ||
| 128 | }, | ||
| 129 | linuxDirectDownload: { | ||
| 130 | name: 'deb', | ||
| 131 | platform: 'linux' | ||
| 132 | } | ||
| 133 | }; | ||
| 134 | const targets = []; | ||
| 135 | console.log(process.platform); | ||
| 136 | switch (process.platform) { | ||
| 137 | case 'win32': | ||
| 138 | // Run in two passes so we can skip signing the AppX for distribution through the MS Store. | ||
| 139 | // targets.push(availableTargets.microsoftStore); | ||
| 140 | targets.push(availableTargets.windowsDirectDownload); | ||
| 141 | break; | ||
| 142 | case 'darwin': | ||
| 143 | // Running 'dmg' and 'mas' in the same pass causes electron-builder to skip signing the non-MAS app copy. | ||
| 144 | // Running them as separate passes means they can both get signed. | ||
| 145 | // Seems like a bug in electron-builder... | ||
| 146 | // Running the 'mas' build first means that its output is available while we wait for 'dmg' notarization. | ||
| 147 | // Add macAppStoreDev here to test a MAS-like build locally. You'll need a Mac Developer provisioning profile. | ||
| 148 | if (fs.existsSync(masDevProfile)) { | ||
| 149 | targets.push(availableTargets.macAppStoreDev); | ||
| 150 | } else { | ||
| 151 | console.log(`skipping target "${availableTargets.macAppStoreDev.name}": ${masDevProfile} missing`); | ||
| 152 | } | ||
| 153 | if (wrapperConfig.doSign) { | ||
| 154 | targets.push(availableTargets.macAppStore); | ||
| 155 | } else { | ||
| 156 | // electron-builder doesn't seem to support this configuration even if mac.type is "development" | ||
| 157 | console.log(`skipping target "${availableTargets.macAppStore.name}" because code-signing is disabled`); | ||
| 158 | } | ||
| 159 | targets.push(availableTargets.macDirectDownload); | ||
| 160 | break; | ||
| 161 | case 'linux': | ||
| 162 | targets.push(availableTargets.linuxDirectDownload); | ||
| 163 | break; | ||
| 164 | default: | ||
| 165 | throw new Error(`Could not determine targets for platform: ${process.platform}`); | ||
| 166 | } | ||
| 167 | return targets; | ||
| 168 | }; | ||
| 169 | |||
| 170 | const parseArgs = function () { | ||
| 171 | const scriptArgs = process.argv.slice(2); // remove `node` and `this-script.js` | ||
| 172 | console.log(scriptArgs); | ||
| 173 | const builderArgs = []; | ||
| 174 | let mode = 'dev'; // default | ||
| 175 | let arch = null; | ||
| 176 | |||
| 177 | for (const arg of scriptArgs) { | ||
| 178 | const modeSplit = arg.split(/--mode(\s+|=)/); | ||
| 179 | const archSplit = arg.split(/--arch(\s+|=)/); | ||
| 180 | if (modeSplit.length === 3) { | ||
| 181 | mode = modeSplit[2]; | ||
| 182 | } else if (archSplit.length === 3) { | ||
| 183 | arch = archSplit[2]; | ||
| 184 | } else { | ||
| 185 | builderArgs.push(arg); | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | let doPackage; | ||
| 190 | let doSign; | ||
| 191 | let doPublish; | ||
| 192 | |||
| 193 | switch (mode) { | ||
| 194 | case 'dev': | ||
| 195 | doPackage = true; | ||
| 196 | doSign = false; | ||
| 197 | doPublish = false; | ||
| 198 | break; | ||
| 199 | case 'dir': | ||
| 200 | doPackage = false; | ||
| 201 | doSign = false; | ||
| 202 | doPublish = false; | ||
| 203 | break; | ||
| 204 | case 'dist': | ||
| 205 | doPackage = true; | ||
| 206 | // doSign = true; // skip code signing before getting a certificate | ||
| 207 | doSign = false; | ||
| 208 | doPublish = false; | ||
| 209 | break; | ||
| 210 | case 'publish': | ||
| 211 | doPackage = true; | ||
| 212 | // doSign = true; // skip code signing before getting a certificate | ||
| 213 | doSign = false; | ||
| 214 | doPublish = true; | ||
| 215 | break; | ||
| 216 | } | ||
| 217 | |||
| 218 | return { | ||
| 219 | builderArgs, | ||
| 220 | doPackage, // false = build to directory | ||
| 221 | doSign, | ||
| 222 | doPublish, | ||
| 223 | mode, | ||
| 224 | arch | ||
| 225 | }; | ||
| 226 | }; | ||
| 227 | |||
| 228 | const main = function () { | ||
| 229 | const wrapperConfig = parseArgs(); | ||
| 230 | console.log(wrapperConfig); | ||
| 231 | // TODO: allow user to specify targets? We could theoretically build NSIS on Mac, for example. | ||
| 232 | wrapperConfig.targets = calculateTargets(wrapperConfig); | ||
| 233 | |||
| 234 | for (const target of wrapperConfig.targets) { | ||
| 235 | runBuilder(wrapperConfig, target); | ||
| 236 | } | ||
| 237 | }; | ||
| 238 | |||
| 239 | main(); |
scripts/fetchMediaLibraryAssets.js
0 → 100644
| 1 | const fs = require('fs'); | ||
| 2 | const https = require('https'); | ||
| 3 | const path = require('path'); | ||
| 4 | const util = require('util'); | ||
| 5 | |||
| 6 | const async = require('async'); | ||
| 7 | |||
| 8 | const libraries = require('./lib/libraries'); | ||
| 9 | |||
| 10 | const ASSET_HOST = 'cdn.assets.scratch.mit.edu'; | ||
| 11 | const NUM_SIMULTANEOUS_DOWNLOADS = 5; | ||
| 12 | const OUT_PATH = path.resolve('static', 'assets'); | ||
| 13 | |||
| 14 | |||
| 15 | const describe = function (object) { | ||
| 16 | return util.inspect(object, false, Infinity, true); | ||
| 17 | }; | ||
| 18 | |||
| 19 | const collectSimple = function (library, dest, debugLabel = 'Item') { | ||
| 20 | library.forEach(item => { | ||
| 21 | let md5Count = 0; | ||
| 22 | if (item.md5) { | ||
| 23 | ++md5Count; | ||
| 24 | dest.add(item.md5); | ||
| 25 | } | ||
| 26 | if (item.baseLayerMD5) { // 2.0 library syntax for costumes | ||
| 27 | ++md5Count; | ||
| 28 | dest.add(item.baseLayerMD5); | ||
| 29 | } | ||
| 30 | if (item.md5ext) { // 3.0 library syntax for costumes | ||
| 31 | ++md5Count; | ||
| 32 | dest.add(item.md5ext); | ||
| 33 | } | ||
| 34 | if (md5Count < 1) { | ||
| 35 | console.warn(`${debugLabel} has no MD5 property:\n${describe(item)}`); | ||
| 36 | } else if (md5Count > 1) { | ||
| 37 | // is this actually bad? | ||
| 38 | console.warn(`${debugLabel} has multiple MD5 properties:\n${describe(item)}`); | ||
| 39 | } | ||
| 40 | }); | ||
| 41 | return dest; | ||
| 42 | }; | ||
| 43 | |||
| 44 | const collectAssets = function (dest) { | ||
| 45 | collectSimple(libraries.backdrops, dest, 'Backdrop'); | ||
| 46 | collectSimple(libraries.costumes, dest, 'Costume'); | ||
| 47 | collectSimple(libraries.sounds, dest, 'Sound'); | ||
| 48 | libraries.sprites.forEach(sprite => { | ||
| 49 | if (sprite.costumes) { | ||
| 50 | collectSimple(sprite.costumes, dest, `Costume for sprite ${sprite.name}`); | ||
| 51 | } | ||
| 52 | if (sprite.sounds) { | ||
| 53 | collectSimple(sprite.sounds, dest, `Sound for sprite ${sprite.name}`); | ||
| 54 | } | ||
| 55 | }); | ||
| 56 | return dest; | ||
| 57 | }; | ||
| 58 | |||
| 59 | const connectionPool = []; | ||
| 60 | |||
| 61 | const fetchAsset = function (md5, callback) { | ||
| 62 | const myAgent = connectionPool.pop() || new https.Agent({keepAlive: true}); | ||
| 63 | const getOptions = { | ||
| 64 | host: ASSET_HOST, | ||
| 65 | path: `/internalapi/asset/${md5}/get/`, | ||
| 66 | agent: myAgent | ||
| 67 | }; | ||
| 68 | const urlHuman = `//${getOptions.host}${getOptions.path}`; | ||
| 69 | https.get(getOptions, response => { | ||
| 70 | if (response.statusCode !== 200) { | ||
| 71 | callback(new Error(`Request failed: status code ${response.statusCode} for ${urlHuman}`)); | ||
| 72 | return; | ||
| 73 | } | ||
| 74 | |||
| 75 | const stream = fs.createWriteStream(path.resolve(OUT_PATH, md5), {encoding: 'binary'}); | ||
| 76 | stream.on('error', callback); | ||
| 77 | response.on('data', chunk => { | ||
| 78 | stream.write(chunk); | ||
| 79 | }); | ||
| 80 | response.on('end', () => { | ||
| 81 | connectionPool.push(myAgent); | ||
| 82 | stream.end(); | ||
| 83 | console.log(`Fetched ${urlHuman}`); | ||
| 84 | callback(); | ||
| 85 | }); | ||
| 86 | }); | ||
| 87 | }; | ||
| 88 | |||
| 89 | const fetchAllAssets = function () { | ||
| 90 | const allAssets = collectAssets(new Set()); | ||
| 91 | console.log(`Total library assets: ${allAssets.size}`); | ||
| 92 | |||
| 93 | async.forEachLimit(allAssets, NUM_SIMULTANEOUS_DOWNLOADS, fetchAsset, err => { | ||
| 94 | if (err) { | ||
| 95 | console.error(`Fetch failed:\n${describe(err)}`); | ||
| 96 | } else { | ||
| 97 | console.log('Fetch succeeded.'); | ||
| 98 | } | ||
| 99 | |||
| 100 | console.log(`Shutting down ${connectionPool.length} agents.`); | ||
| 101 | while (connectionPool.length > 0) { | ||
| 102 | connectionPool.pop().destroy(); | ||
| 103 | } | ||
| 104 | }); | ||
| 105 | }; | ||
| 106 | |||
| 107 | fetchAllAssets(); |
scripts/lib/libraries.js
0 → 100644
| 1 | const backdrops = require('openblock-gui/src/lib/libraries/backdrops.json'); | ||
| 2 | const costumes = require('openblock-gui/src/lib/libraries/costumes.json'); | ||
| 3 | const sounds = require('openblock-gui/src/lib/libraries/sounds.json'); | ||
| 4 | const sprites = require('openblock-gui/src/lib/libraries/sprites.json'); | ||
| 5 | |||
| 6 | const libraries = { | ||
| 7 | backdrops, | ||
| 8 | costumes, | ||
| 9 | sounds, | ||
| 10 | sprites | ||
| 11 | }; | ||
| 12 | |||
| 13 | module.exports = libraries; |
src/.eslintrc.js
0 → 100644
src/common/ElectronStorageHelper.js
0 → 100644
| 1 | const fs = require('fs'); | ||
| 2 | const path = require('path'); | ||
| 3 | |||
| 4 | const staticAssets = path.resolve(__static, 'assets'); | ||
| 5 | |||
| 6 | /** | ||
| 7 | * Allow the storage module to load files bundled in the Electron application. | ||
| 8 | */ | ||
| 9 | class ElectronStorageHelper { | ||
| 10 | constructor (storageInstance) { | ||
| 11 | this.parent = storageInstance; | ||
| 12 | } | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Fetch an asset but don't process dependencies. | ||
| 16 | * @param {AssetType} assetType - The type of asset to fetch. | ||
| 17 | * @param {string} assetId - The ID of the asset to fetch: a project ID, MD5, etc. | ||
| 18 | * @param {DataFormat} dataFormat - The file format / file extension of the asset to fetch: PNG, JPG, etc. | ||
| 19 | * @return {Promise.<Asset>} A promise for the contents of the asset. | ||
| 20 | */ | ||
| 21 | load (assetType, assetId, dataFormat) { | ||
| 22 | assetId = path.basename(assetId); | ||
| 23 | dataFormat = path.basename(dataFormat); | ||
| 24 | |||
| 25 | return new Promise((resolve, reject) => { | ||
| 26 | fs.readFile( | ||
| 27 | path.resolve(staticAssets, `${assetId}.${dataFormat}`), | ||
| 28 | (err, data) => { | ||
| 29 | if (err) { | ||
| 30 | reject(err); | ||
| 31 | } else { | ||
| 32 | resolve(new this.parent.Asset(assetType, assetId, dataFormat, data)); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | ); | ||
| 36 | }); | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | module.exports = ElectronStorageHelper; |
src/common/log.js
0 → 100644
| 1 | import minilog from 'minilog'; | ||
| 2 | minilog.enable(); | ||
| 3 | |||
| 4 | const namespace = (() => { | ||
| 5 | switch (process.type) { | ||
| 6 | case 'browser': return 'main'; | ||
| 7 | case 'renderer': return 'web'; | ||
| 8 | default: return process.type; // probably 'worker' for a web worker | ||
| 9 | } | ||
| 10 | })(); | ||
| 11 | |||
| 12 | export default minilog(`app-${namespace}`); |
src/icon/OpenBlockDesktop.png
0 → 100644
51.7 KB
src/icon/OpenBlockDesktop.svg
0 → 100644
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.78 306.36"><defs><style>.a{fill:#ffbc47;}.b{fill:#cf8b17;}.c{fill:#ffab19;}.d{fill:#fff;}</style></defs><title>OpenBlock-Desktop</title><path class="a" d="M0,236.43c0-2,0-4,0-6Q0,128,0,25.51a37.76,37.76,0,0,1,.66-7.63A22,22,0,0,1,22.63.06H176.68c28.08,0,56.15.06,84.23-.06a22.33,22.33,0,0,1,22.83,22.42q-.08,98-.05,196.05c0,6.4-.06,12.79-.09,19.19-2.36,11-11,17.94-22.26,18-12.39,0-24.79.13-37.18-.06a11,11,0,0,0-9.75,4.9c-5.36,7.17-11,14.11-16.54,21.17a27.39,27.39,0,0,1-3.63,3.81,10.93,10.93,0,0,1-7.43,2.84q-44.73,0-89.46,0c-4.64,0-8-2.29-10.77-5.8-5.61-7.18-11.29-14.31-16.83-21.55a12.39,12.39,0,0,0-11-5.39c-12.15.28-24.3.22-36.45.05C13,255.46,6.19,251,1.75,242.93A24.38,24.38,0,0,1,0,237.38"/><path class="b" d="M0,249.14"/><path class="b" d="M0,237.92a17.51,17.51,0,0,0,3,6c4.41,7,10.68,10.89,19,11,12.63.13,25.27.21,37.9,0,4.49-.09,7.42,1.85,9.94,5.07,6,7.66,11.87,15.39,17.94,23a12.14,12.14,0,0,0,10,4.75c29.1-.08,58.21-.15,87.31.05,5.37,0,8.94-2.26,12-6.12,5.14-6.5,10.23-13,15.36-19.54a43.75,43.75,0,0,1,3.57-4.49,8.71,8.71,0,0,1,6.43-2.65c13,.06,25.91,0,38.86,0a22.25,22.25,0,0,0,21.38-15.1,5.42,5.42,0,0,1,.93-2.17c0,4.55.32,9.12.08,13.66a21.72,21.72,0,0,1-20.46,20.46c-11.26.56-22.54.27-33.81.14-4.13,0-7.4,1.07-10.12,4.3-7.42,8.8-15,17.47-22.5,26.2a10.49,10.49,0,0,1-8.55,3.92q-46.3-.13-92.6,0a10.73,10.73,0,0,1-8.74-4.06c-7.23-8.44-14.6-16.76-21.68-25.33-3.1-3.75-6.68-5.16-11.48-5-10.15.26-20.31.14-30.46.08-6.66,0-12.58-2.08-17.23-7.07-3.14-3.36-6-8.23-6-12.94"/><path class="c" d="M259.35,12.27a22.72,22.72,0,0,1,5.9.73c4.89,1.31,7.29,4.92,7.81,10a11.82,11.82,0,0,1,0,1.2q0,103.78,0,207.56a11.93,11.93,0,0,1-5.07,9.81,8.77,8.77,0,0,1-5.63,1.58c-14.32,0-28.63,0-43,0a12.51,12.51,0,0,0-10.79,5.44c-3.76,5-7.79,9.86-11.68,14.8-1.87,2.37-3.63,4.83-5.53,7.19-1.75,2.17-3.49,4.34-6.49,5a14.73,14.73,0,0,1-3.08.28H107c-1.43,0-2.86.19-4.27.27a11.15,11.15,0,0,1-6.57-1.72c-2.34-1.46-3.64-3.8-5.18-5.87-4.23-5.68-8.84-11.06-13.09-16.72a30.58,30.58,0,0,0-5.76-6.31,10.55,10.55,0,0,0-6.62-2.31c-14.32,0-28.64-.09-43,.06A11.47,11.47,0,0,1,11,231.65q.13-52.19,0-104.38h0c0-34.63.12-69.27-.11-103.9A11.29,11.29,0,0,1,21.66,12.31"/><path class="d" d="M71.1,101.89c-3,0-5.92,0-8.88,0-3.25,0-5.47-1.88-7.28-4.31a4.78,4.78,0,0,1-.7-3c0-5,0-10.08,0-15.12a5.94,5.94,0,0,1,3.12-5.24A14,14,0,0,1,64.8,72.2c5.28,0,10.56,0,15.84,0,1.16,0,1.72-.18,2.15-1.52A28,28,0,0,1,86,63.93a21,21,0,0,1,6.18-5.8,15.34,15.34,0,0,1,8.92-2.59q43.44.05,86.88,0a17.88,17.88,0,0,1,9.71,3.11c3.84,2.39,6,6,7.83,9.87a1.27,1.27,0,0,1,.19.43c0,1.12.07,2.39,1.06,3s2.19.22,3.29.23c3.52,0,7,.13,10.56,0a16.84,16.84,0,0,1,6.53.92c3.24,1.18,5.64,3.1,5.56,7.33-.08,4.64-.14,9.28,0,13.92.1,3-1.45,4.76-3.68,6.21a8.81,8.81,0,0,1-5.18,1.38c-5.2-.12-10.4-.05-15.6,0-1.84,0-1.87,0-1.88,1.87,0,3.6,0,7.2,0,10.8,0,2.14,0,2.16,2.31,2.16q7,0,13.92,0a13.53,13.53,0,0,1,7.39,2.17,6,6,0,0,1,2.66,5.09c.07,5.28,0,10.56,0,15.84a4.51,4.51,0,0,1-1.35,3.2,9.64,9.64,0,0,1-7.76,3.42c-5-.18-10.08-.06-15.12,0-2,0-2.06,0-2.06,1.91q0,5.88,0,11.76c0,1.7,0,1.75,1.75,1.75,5.44,0,10.88.06,16.32,0,3.39,0,5.66,1.81,7.59,4.23a4.64,4.64,0,0,1,.66,3c0,4.8-.09,9.6,0,14.4.09,4.15-2.5,6.09-5.65,7.29a16.35,16.35,0,0,1-6.31.83c-4.16-.11-8.32,0-12.48,0-1.67,0-2.11.35-2.46,1.9a19.63,19.63,0,0,1-18.76,15c-29-.1-57.92-.06-86.88,0A14.11,14.11,0,0,1,93,206a22.82,22.82,0,0,1-4.12-2.77A23.43,23.43,0,0,1,82.73,193c-.39-1.21-.85-1.45-1.92-1.45-5.52,0-11,0-16.56,0a12.12,12.12,0,0,1-8-3,5.62,5.62,0,0,1-2-4.19c0-5.28,0-10.56,0-15.84a4.37,4.37,0,0,1,1.23-3,9.67,9.67,0,0,1,7.89-3.67c5.75.19,11.52.06,17.28,0,1.87,0,1.9,0,1.91-1.82,0-4,0-8,0-12,0-1.27-.45-1.64-1.67-1.63-6.16,0-12.32,0-18.48,0-3.48,0-5.72-2-7.62-4.57a4.35,4.35,0,0,1-.58-2.75c0-4.8,0-9.6,0-14.4.06-4.42,2.82-6.32,6.2-7.35,2.72-.83,5.51-.59,8.28-.6,3.92,0,7.84,0,11.76,0,2,0,2,0,2.06-1.92,0-3.76,0-7.52,0-11.28,0-1.25-.42-1.68-1.66-1.65C77.66,101.93,74.38,101.89,71.1,101.89Z"/><path class="c" d="M109.23,93.44c-6.7.4-12.82-5.25-13.19-12.18a12.93,12.93,0,0,1,25.79-1.74A12.89,12.89,0,0,1,109.23,93.44Z"/></svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/icon/OpenBlockLoading.svg
0 → 100644
This diff could not be displayed because it is too large.
src/icon/logo-OpenBlockcc.svg
0 → 100644
This diff could not be displayed because it is too large.
src/icon/logo-ScratchFoundation.svg
0 → 100644
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | ||
| 3 | <svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||
| 4 | viewBox="0 0 2031.7 868" style="enable-background:new 0 0 2031.7 868;" xml:space="preserve"> | ||
| 5 | <style type="text/css"> | ||
| 6 | .st0{fill:#96999B;} | ||
| 7 | .st1{fill:#FCE4CE;} | ||
| 8 | .st2{fill:#B5B7B9;} | ||
| 9 | .st3{fill:#FDEBDB;} | ||
| 10 | .st4{fill:#D1D3D3;} | ||
| 11 | .st5{fill:#B3B5B7;} | ||
| 12 | .st6{fill:#F89C49;} | ||
| 13 | .st7{fill:#979A9C;} | ||
| 14 | .st8{fill:#FDFDFD;} | ||
| 15 | .st9{fill:#FEFEFE;} | ||
| 16 | </style> | ||
| 17 | <g> | ||
| 18 | <path class="st0" d="M1669.7,868c-2.4-2.2-5.5-1.4-8.3-2.1c-31.4-7.8-52.6-27-64.4-56.9c-3.7-9.4-5.9-19-6.5-29.1 | ||
| 19 | c-0.9-3.3,0.4-6.9-0.8-10.2c0-0.5,0-1,0-1.5c1.1-3.3-0.2-6.8,0.7-10.2c1.4-19.9,8-37.8,20.2-53.6c13.3-17.3,30.7-28.1,52.2-32.3 | ||
| 20 | c1.9-0.4,3.9-0.8,5.8-1.2c1.6-0.9,3.5,0.2,5.1-0.8c2-0.1,3.9-0.2,5.9-0.2c3.6-0.5,7.3-0.5,10.9,0c23.8,1.6,45,9.6,61.9,26.8 | ||
| 21 | c14.6,14.8,22.9,33,26.2,53.5c0.4,2.3,0.8,4.6,1.2,6.9c0.8,2-0.3,4.2,0.7,6.2c0,3.8,0,7.7,0,11.5c-1,2,0.1,4.2-0.7,6.2 | ||
| 22 | c-2.6,19-8.3,36.6-20.2,52c-12.4,16.1-28.5,26.4-47.8,31.9c-4,1.1-8.1,2.1-12.2,3.2C1689.7,868,1679.7,868,1669.7,868z"/> | ||
| 23 | <path class="st0" d="M260.7,867.1c-34.5-4.7-59.6-22.9-72.7-55.1c-15-37.1-12.3-73.5,11.6-106.6c14.8-20.5,35.6-31.4,60.6-34.6 | ||
| 24 | c22.5-2.9,43.8,0.2,63.5,11.9c22.6,13.4,36.2,33.5,42.3,58.7c6.9,29,4,56.9-11.3,82.8c-14.6,24.6-36.7,38.3-64.8,42.8 | ||
| 25 | c-1.1,0.2-2.4-0.2-3.1,1c-7,0-14,0-21,0C264.2,866.8,262.2,868.1,260.7,867.1z"/> | ||
| 26 | <path class="st0" d="M495.7,868c-2.9-2.2-6.6-1.9-9.9-2.7c-29.9-7.5-48.3-28.1-52.2-58.6c-0.7-5.1-1-10.3-1-15.4 | ||
| 27 | c0-35.8,0-71.7,0-107.5c0-6.4,2.7-10.2,7.9-11.9c5.6-1.7,10.5-0.3,13.9,4c2,2.7,2.3,5.8,2.3,9c0,36-0.1,72,0.1,108 | ||
| 28 | c0,11.1,2.6,21.8,8.5,31.4c8.2,13.4,20.5,19.8,35.7,21.8c9.2,1.2,18.3,0.8,27.2-1.5c19.9-5.1,33.6-22.2,35.6-43.4 | ||
| 29 | c0.7-7,0.8-14,0.8-20.9c0-32.3,0-64.7,0-97c0-5.9,2.8-9.7,8-11.3c5.7-1.7,10.4-0.3,13.8,4.1c1.7,2.2,2.3,4.7,2.2,7.5 | ||
| 30 | c0,37,0.2,74-0.2,111c-0.1,14.3-3.3,28.1-10.6,40.6c-11.4,19.6-29.5,28.6-51.1,31.8c-1.1,0.2-2.4-0.2-3.1,1c-0.7,0-1.3,0-2,0 | ||
| 31 | c-1.7-0.8-3.3-0.8-5,0c-4.3,0-8.7,0-13,0c-2-0.8-4-0.8-6,0C497.1,868,496.4,868,495.7,868z"/> | ||
| 32 | <path class="st1" d="M2031.7,463c-0.3,0-0.6,0.1-0.9,0c-2-2.4-2-4.7,0-7.1c0.3,0,0.6,0,0.9,0C2031.7,458.3,2031.7,460.7,2031.7,463 | ||
| 33 | z"/> | ||
| 34 | <path class="st2" d="M497.7,868c2-1.7,4-1.4,6,0C501.7,868,499.7,868,497.7,868z"/> | ||
| 35 | <path class="st3" d="M983.7,0.8c0-0.3,0-0.6,0-0.8c1.7,0,3.3,0,5,0c0,0.3,0,0.6,0,0.8C987.1,2.5,985.4,2.5,983.7,0.8z"/> | ||
| 36 | <path class="st4" d="M260.7,867.1c1.8,0,3.6-0.7,5.1,0.9c-1.7,0-3.3,0-5,0C260.7,867.7,260.7,867.4,260.7,867.1z"/> | ||
| 37 | <path class="st5" d="M516.7,868c1.7-1.4,3.3-1.5,5,0C520.1,868,518.4,868,516.7,868z"/> | ||
| 38 | <path class="st6" d="M2030.9,456c0,2.4,0,4.7,0,7.1c-2.4,9.8-8.1,16.5-18.2,19.1c-1.6,0.8-3.5-0.2-5.2,0.8 | ||
| 39 | c-2.3,0.1-4.6,0.2-6.8,0.3c-2,0.8-4.2-0.3-6.2,0.7c-1.9,0.1-3.9,0.2-5.8,0.3c-2,0.8-4.2-0.3-6.2,0.8c-1.6,0.1-3.2,0.2-4.8,0.2 | ||
| 40 | c-1.7,0.8-3.6-0.2-5.2,0.7c-11,1.3-22.1,2.4-33.1,4c-12.8,1.8-25.6,4.1-38,8c-5.7,1.8-11.2,4.1-16.8,5.9 | ||
| 41 | c-10.4,3.4-17.2,0.5-23.3-7.6c-5.2-6.9-3.8-22.1,1.4-27.7c9-9.8,20.8-13.5,33-16.3c10.3-2.4,20.7-4.3,31.1-6.4 | ||
| 42 | c1.5-0.7,1.5-2,1.5-3.4c0-20.8,0-41.5,0-62.3c0-0.5-0.2-1-0.4-1.4c-0.1-29.9-0.2-59.9-0.3-89.8c-0.7-4.7-0.2-9.4-0.3-14.2 | ||
| 43 | c0-2.9-0.9-4-3.8-2.9c-11.1,1.7-22.3,3.3-33.4,5.1c-23.2,3.9-46.3,7.9-69.5,11.8c-8.2,1.4-16.4,2.8-24.7,4 | ||
| 44 | c-2.5,0.4-3.6,1.4-3.5,3.9c0.1,3,0.1,6,0.2,9c-0.6,1.1-0.4,2.3-0.4,3.4c0,32.3,0,64.6,0,96.9c0,1.3-0.4,2.7,0.5,3.9l0,0 | ||
| 45 | c7.2,1.2,14.4,0.2,21.6,0.5c3.1,0.1,6.2-0.4,9.3,0.3c3.8,0.2,7.7,0,11.3,0.7c9.8,1.9,15.7,8.3,18.8,17.5c0.6,3.3,0.6,6.6,0,9.9 | ||
| 46 | c-3.3,9.9-9.6,16.4-20.2,18.2c-3.6,0.5-7.3,0.5-10.9,0c-10.6-0.1-21.2-0.2-31.8-0.2c-8.2-1-16.5-0.2-24.7-0.5 | ||
| 47 | c-2.5-0.1-5,0.3-7.4-0.3c-11-0.2-22-0.5-33-0.5c-11.8,0.1-22.3-7.6-23.8-18.6c-1.6-11.2,3.8-23.6,15.5-26.5 | ||
| 48 | c0.8-0.2,1.5-0.5,2.3-0.8c6.8-0.8,13.7-0.1,20.6-0.4c1.8-0.1,3.6,0.5,5.3-0.5c0.7-6.6,0.4-13.1,0.2-19.7c0.8-1.6,0.4-3.3,0.4-4.9 | ||
| 49 | c0-91.4,0-182.8,0-274.1c0-5.7,0-5.7-5.5-5.7c-11.6-0.2-23.3,0.4-34.9-0.3c-11.8-0.7-21.8-11.5-19.9-26.1 | ||
| 50 | c1.4-10.8,9.3-19.9,23-19.9c39.9-0.1,79.9-0.1,119.8,0c11.4,0,21.7,8.8,23.1,18.7c2,14.4-6.8,25.3-19.1,27.3 | ||
| 51 | c-11.4,0.8-22.9,0.1-34.4,0.4c-1.8,0-3.7-0.5-5.4,0.5c-0.1,16.3-0.3,32.6-0.3,48.9c-0.1,29.5,0,58.9-0.2,88.4 | ||
| 52 | c0,4.2,1.3,4.4,4.7,3.8c20.4-3.5,40.8-6.8,61.2-10.3c21.6-3.6,43.1-7.2,64.7-10.9c1.3-0.2,2.5-0.7,3.8-1.1 | ||
| 53 | c0.9-10.4,0.8-20.7,0.1-31.1c-0.1-26.6-0.2-53.2-0.2-79.8c-0.7-13.9-0.1-27.8-0.3-41.7c0-1.8,0.4-3.6-0.5-5.4l0,0 | ||
| 54 | c-4.8-0.8-9.6-0.7-14.3,0c-7.6,0-15.3-0.1-22.9,0.1c-10.6,0.3-18.1-4.8-22.1-14.1c-3.7-8.7-2.1-17.5,4.2-24.8 | ||
| 55 | c4.1-4.7,9.9-6.9,16.2-7.3c6.6-0.6,13.2-0.1,19.8-0.3c1.8-0.1,3.6,0.4,5.4-0.4c9.6-0.1,19.2-0.2,28.8-0.2 | ||
| 56 | c7.9-0.7,15.9-0.1,23.8-0.3c1.8-0.1,3.6,0.4,5.4-0.4c9.9-0.1,19.9-0.2,29.8-0.3c3.3-0.5,6.6-0.5,9.9,0c20,3.8,23.7,22.1,18.2,33.2 | ||
| 57 | c-4.2,8.3-11.1,12.7-20.4,12.9c-10,0.3-20,0.4-30,0.3c-3.9-0.1-5,1.2-5,5.1c0.3,45.3,0.1,90.6,0.7,135.9 | ||
| 58 | c0.7,57.6,0.4,115.3,0.9,172.9c0.2,18.8,0.4,37.6,0.4,56.5c0,4,1.3,4.7,4.9,4.3c8.8-0.9,17.6-1.7,26.4-2.1 | ||
| 59 | C2019.2,436.1,2027.7,442.7,2030.9,456z"/> | ||
| 60 | <path class="st6" d="M570.8,449.8c-9.3-10.2-11.1-21.3-4.8-30.8c3.6-5.4,8.5-8.9,14.9-10c8.9-1.5,17.7-3,26.7-3.9 | ||
| 61 | c4.7-0.5,6.2-2.1,6.2-7.1c-0.5-40.7-0.7-81.3-0.8-122c0-4.7-1.5-5.6-5.7-5.2c-5.8,0.6-11.6,0.2-17-2.9 | ||
| 62 | c-8.9-5.1-13.7-15.7-11.2-25.6c2.5-9.9,11.1-16.9,21.3-17.4c3.9-0.2,9,1.6,11.5-1.1c2.5-2.7,0.7-7.6,0.7-11.6c0-29.7-0.2-59.3,0-89 | ||
| 63 | c0-5.2-1.9-5.7-6.1-5.1c-9.2,1.3-18.5,2.4-27.7,3.7c-14.7,2-26.1-5.5-28.2-18.7c-2.2-13.6,6.5-24.9,21-26.9 | ||
| 64 | c38.5-5.3,77.1-8.8,116-8c29.7,0.7,59.2,3.1,87.6,12.7c4.6,1.5,9,3.5,13.5,5.2c7,4,14.2,7.6,20.3,13.1c14.9,13.3,22,29.8,20,49.9 | ||
| 65 | c-2.2,21.8-13.1,39.6-25.7,56.6c-5.9,7.9-13.1,14.5-21,20.3c-1.8,1.3-4.4,2.3-1.8,5.7c10.2,13.4,14.8,29.4,19.2,45.3 | ||
| 66 | c4.9,17.7,7.3,35.9,10.2,54c0.8,1.7-0.2,3.6,0.8,5.2c0.1,1.6,0.2,3.2,0.3,4.8c0.7,1.7-0.2,3.6,0.7,5.2c0.1,2.3,0.2,4.5,0.3,6.8 | ||
| 67 | c0.8,2.3-0.3,4.9,0.7,7.2c0.1,3.3,0.2,6.5,0.3,9.8c0.9,4-0.4,8.2,0.7,12.2c0.1,2.6,0.2,5.2,0.3,7.8c0.7,1.7-0.2,3.6,0.7,5.2 | ||
| 68 | c1.1,18,3.2,35.8,8.3,53.1c2.6,8.9,8.1,15.5,18.6,15.7c2.8,0.7,5.6,0.7,8.4,0c10-3.2,14.3-11.5,17.8-20.4 | ||
| 69 | c2.4-5.9,3.6-12.2,7.3-17.6c6-8.6,17.8-11.5,28.3-7c9,3.8,15.5,15.2,13,24.5c-4.9,18.2-11.7,35.7-26,48.9 | ||
| 70 | c-12.8,11.8-27.8,19-45.7,18.3c-19.3-0.7-37-5.6-50.3-20.8c-9.5-10.8-15.6-23.3-18.6-37.4c-2.7-12.9-4.8-25.9-6.2-39 | ||
| 71 | c-0.4-3.9-0.3-7.9-1.1-11.8c-0.9-2,0.3-4.2-0.8-6.2c-0.1-2-0.2-3.9-0.2-5.9c-0.9-2.7,0.3-5.6-0.7-8.2c-0.1-4.3-0.2-8.5-0.3-12.8 | ||
| 72 | c-0.9-3,0.4-6.2-0.8-9.2c-0.1-1.9-0.2-3.9-0.2-5.8c-0.8-2,0.2-4.2-0.7-6.2c-0.1-1.6-0.2-3.2-0.3-4.8c-0.8-1.7,0.2-3.6-0.8-5.2 | ||
| 73 | c-2.7-22.6-6.2-45-13.3-66.7c-1.6-0.8-2.2-2.4-3-3.9c-6.1-10.8-17.7-16.2-29.6-13c-17.3,4.8-34.8,8.8-52.5,11.8 | ||
| 74 | c-3.9,0.7-5.2,2.8-5.2,6.5c0.1,25.9,0,51.9,0,77.8c0,3.5,2,7.2-0.6,10.6c0,10.8,0.1,21.6-0.1,32.4c-0.1,3.9,1.8,3.9,4.6,3.5 | ||
| 75 | c8.7-1.3,17.4-2.4,26-3.9c14.4-2.6,25.2,5.5,28.3,17.7c0.1,0.5,0.3,1,0.4,1.4c0.6,2,0.5,3.9,0,5.9c-3.5,13.5-11.4,19.3-25.1,21.2 | ||
| 76 | c-16.6,2.3-33.1,4.5-49.6,7.4c-9.8,1.7-19.9,2.5-29.7,4.3c-8.4,1.5-17,2-25.4,3.8C582.5,456,576.1,454.6,570.8,449.8z"/> | ||
| 77 | <path class="st6" d="M983.7,0.8c1.7,0,3.4,0,5.1,0c12,1.8,18.1,10.2,22.3,20.4c13.7,33.1,27.2,66.3,40.9,99.4 | ||
| 78 | c14.8,35.7,29.8,71.3,44.5,107.1c9.2,22.4,18.3,45,27.2,67.5c1.5,3.9,2.8,5.3,7.2,3.1c10.1-5,20.5-9.4,30.9-13.9 | ||
| 79 | c16.3-7.2,31.2,0.2,35.2,15.7c0.4,3,0.5,5.9,0,8.9c-3.1,13.1-13.8,17.1-24.8,21c-18.8,6.8-37.2,14.5-54.9,23.8 | ||
| 80 | c-12.6,6.6-24.9,13.7-34.6,24.7c-6.4,7.2-16.2,8.5-25.6,4.5c-6.9-2.9-13-11.3-13.1-20.3c-0.1-11.4,6.5-18.7,14.4-25.2 | ||
| 81 | c7-5.8,15-10.4,23.1-14.6c2.4-1.2,2.7-2.3,1.6-5c-5.6-13.4-10.8-26.9-16.2-40.4c-0.9-2.3-2.5-4.4-2.8-6.7c-0.8-5.1-3.9-5.4-7.9-4.8 | ||
| 82 | c-32.1,5.3-64.1,10.7-96.2,16c-2.8,0.5-4,1.5-4.4,4.7c-1.1,9.3-3.4,18.5-3.8,28c-1.5,3.5,1.7,2.4,3.1,3.1 | ||
| 83 | c10.8,2,17.3,8.4,19.2,19.2c0.4,2.3,0.5,4.6,0,6.9c-3.9,14.1-11,19.4-26.9,20c-3-1.1-6.2,0.2-9.2-0.7c-2.9-0.1-5.9-0.2-8.8-0.3 | ||
| 84 | c-3-1.1-6.2,0.1-9.2-0.7c-3.3-0.1-6.5-0.2-9.8-0.3c-3-1.1-6.2,0.2-9.2-0.7c-2.9-0.1-5.9-0.2-8.8-0.2c-3-1.1-6.2,0.2-9.2-0.7 | ||
| 85 | c-2.9-0.1-5.9-0.2-8.8-0.3c-1.6-1-3.5,0-5.2-0.7c-9.8-1.3-16.7-8.4-18.3-18.3c-0.4-3-0.5-5.9,0-8.9c1.9-10.3,8.2-16.1,18.3-18.2 | ||
| 86 | c4-0.5,7.9-0.5,11.9,0c2.6,0.1,5.2,0.2,7.8,0.3c2.3,1,4.9-0.1,7.3,0.7c1.5,0.1,3,0.1,4.5,0.4c3.1,0.4,4.3-0.8,4.9-4 | ||
| 87 | c4.7-24.4,9.8-48.7,14.6-73c4.8-24.1,9.5-48.1,14.3-72.2c3-14.9,5.9-29.9,8.8-44.8c1.7-20.6,7.6-40.4,11-60.7 | ||
| 88 | c0.3-1.5,0.6-2.9,0.8-4.4c0.3-2.2-0.5-3.1-2.7-2.9c-3,0.2-6,0.6-8.9,1.1c-16.1,2.3-32.3,4.3-48.4,6.8c-3.4,0.5-6.6,0.3-9.8-0.8 | ||
| 89 | c-13.2-1.5-18.9-11.5-20-21.2c1-15.6,8.4-23,24.2-25.1C920.6,9.7,952.2,5.2,983.7,0.8z"/> | ||
| 90 | <path class="st6" d="M1338,422.1c8.8,0,17.6,0.4,26.4-0.1c12.6-0.8,23.9,5.1,27.5,19.1c0.5,2.6,0.5,5.3,0,7.9 | ||
| 91 | c-3.8,13.2-12.5,19.6-26.2,19.2c-9.3,0.7-18.5,0.1-27.8,0.3c-1.8,0-3.6-0.4-5.4,0.4c-10.9,0.1-21.9,0.2-32.8,0.3 | ||
| 92 | c-9.3,0.7-18.5,0.1-27.8,0.3c-1.8,0-3.6-0.4-5.4,0.4c-10.9,0.1-21.9,0.2-32.8,0.3c-2,0.5-3.9,0.5-5.9,0 | ||
| 93 | c-11.7-1.1-19.7-8.4-21.2-19.2c-0.5-2.6-0.5-5.3,0-7.9c2-11.6,9.9-18.2,21.8-19c3.5-0.2,7-0.2,10.5-0.3c10.8-0.7,21.6-0.1,32.3-0.3 | ||
| 94 | c1.8,0,3.7,0.5,5.4-0.5c0.2-1.5,0.5-2.9,0.5-4.4c-0.1-75.4-0.2-150.8-0.3-226.2c0-0.8-0.1-1.7-0.2-2.5c-0.7-17-0.1-34.1-0.3-51.1 | ||
| 95 | c-0.1-4.9,0.6-9.9-0.5-14.8c-4,0-8,0.1-12.1,0.1c-5.4,0.9-10.8-0.4-16.2,0.7c-5.6,0.1-11.2,0.2-16.8,0.3 | ||
| 96 | c-5.1,0.9-10.2-0.4-15.3,0.7c-5,0-10,0.3-14.9,0c-3.7-0.2-4.2,1.6-3.7,4.5c1.5,9.1,3,18.2,4.5,27.3c2.4,14.6,5.3,29.2,6.8,43.9 | ||
| 97 | c1,9.2-5.4,19.4-18.4,22.7c-3,0.4-5.9,0.5-8.9-0.1c-10.7-1.9-17.3-10.1-18.9-21.4c-1.7-11.7-3.8-23.4-5.7-35.1 | ||
| 98 | c-1.8-11.4-3.6-22.8-5.2-34.2c-1.3-9.3-3.3-18.6-4.2-28c-1.5-15,9.8-25.6,24.1-25.4c4.3,0.1,8.6-0.2,13-0.3 | ||
| 99 | c7.1-1,14.2,0.4,21.2-0.7c7.3-0.1,14.5-0.2,21.8-0.3c7.1-1,14.2,0.4,21.2-0.7c7.3-0.1,14.5-0.2,21.8-0.3c7.4-1,14.8,0.5,22.2-0.8 | ||
| 100 | c6.9-0.1,13.9-0.2,20.8-0.3c6.4-1,12.8,0.4,19.2-0.7c5.3-0.1,10.6-0.2,15.8-0.3c4.4-0.9,8.8,0.4,13.2-0.7c3.9-0.1,7.9-0.2,11.8-0.3 | ||
| 101 | c3.7-0.9,7.5,0.4,11.2-0.7c2.9-0.1,5.9-0.2,8.8-0.2c3-0.9,6.2,0.3,9.2-0.7c2.6-0.1,5.2-0.2,7.8-0.2c2.3-0.9,4.9,0.3,7.2-0.7 | ||
| 102 | c15.2,0.4,27.9,9.9,25.7,30.7c-1.1,3.3,0.1,6.8-0.7,10.2c-0.5,8.4-0.5,16.7,0,25.1c0.9,2.7-0.4,5.5,0.8,8.1 | ||
| 103 | c0.4,8.7-1,16.7-7.7,23.1c-3.5,3.4-7.8,4.7-12.3,5.9c-2.3,0.5-4.6,0.5-6.9,0c-11.4-0.4-18.5-12.7-18.9-21.3c0-1-0.2-2-0.3-3 | ||
| 104 | c-0.9-4.4,0.4-8.8-0.7-13.2c0.6-2.8-0.1-5.3-1.2-7.8c-0.5-1.6-0.5-3.3,0-4.9c0-1.4,0-2.7,0.1-4.1c-3-0.9-6.1-0.7-9.1-0.1 | ||
| 105 | c-3.6,0.1-7.2,0.2-10.8,0.3c-4.4,0.9-8.8-0.4-13.2,0.7c-5.6,0.1-11.2,0.2-16.8,0.3c-5.7,1-11.5-0.5-17.2,0.7 | ||
| 106 | c-3.5-0.8-5,0.2-4.9,4.2c0.3,13.6,0.2,27.3,0.3,40.9c0.6,2,0.3,4,0.3,6c0,80.7,0,161.4,0,242.1c0,2-0.5,4,0.5,5.9l0,0 | ||
| 107 | C1328.5,422.9,1333.3,422.7,1338,422.1z"/> | ||
| 108 | <path class="st6" d="M1578.5,484.9c-4.3,0.1-8.6,0.2-12.8,0.2c-10,0.5-19.9,0.5-29.9,0c-22.2-0.6-42.3-7.3-60.1-20.4 | ||
| 109 | c-10.6-7.8-18.8-17.8-25.7-29c-8.5-13.5-13-28.4-16-43.9c-0.8-3.9-0.3-8-0.4-11.9c-0.7-5.9-0.1-11.9-0.3-17.8 | ||
| 110 | c-0.1-1.8,0.4-3.6-0.4-5.4c0-12.2,0-24.4,0-36.5c1-8.9,0.2-17.9,0.4-26.8c0.1-2.5-0.3-5,0.3-7.4c0.1-4.2,0.2-8.5,0.3-12.7 | ||
| 111 | c0.9-1.7,0-3.6,0.7-5.3c1.3-17.4,4.7-34.4,10.2-51.1c4.9-14.7,12.2-28,23-39.1c10.4-10.7,23.3-17.4,38.2-19.8 | ||
| 112 | c2.9-0.5,5.9-0.9,8.9-1.3c4.6-0.5,9.3-0.5,13.9,0c27.1,3.2,52.1,12.3,74.4,28.1c0.8,0.6,1.7,0.9,2.6,1.4c1.3-3,0.5-6.1,0.5-9.2 | ||
| 113 | c0.3-11-0.4-22.1,0.4-33.1c1.4-11.3,8.8-18.9,20.2-20.2c2-0.5,3.9-0.5,5.9,0c15.7,5.5,20,11.5,20,28.2c0,30.6,0,61.3,0,91.9 | ||
| 114 | c0,14.3-7.9,24.2-22.9,25.6c-7.6,0.7-15.2-2.9-19.7-10.2c-7.1-11.7-16.2-21.8-26.3-30.7c-11.7-10.4-25.5-17.6-40.7-21.7 | ||
| 115 | c-9.4-2.5-18.7-5.4-28.5-3.5c-8.6,1.7-14.9,6.6-19.3,14.2c-7.9,13.7-11.4,28.8-13.6,44.3c-0.6,4.3-1.5,8.5-1.1,12.8 | ||
| 116 | c-1,2,0.1,4.2-0.7,6.2c-0.1,13.3-0.2,26.5-0.3,39.8c-0.7,11.8-0.7,23.7,0,35.5c0.1,15.5-1.1,31,4.9,46.1 | ||
| 117 | c7.7,19.6,27.8,35.3,47.1,35.7c1.6,1.1,3.5,0,5.1,0.8c11.4,0.6,22.7,0.3,34.1,0c2.3-0.8,4.9,0.3,7.2-0.7c11-2.5,20.9-7,28.3-15.8 | ||
| 118 | c4.2-5,8.6-9.9,11.5-16c3.4-7.1,5-14.7,6.1-22.5c0.2-1.3,0.6-2.5,0.9-3.7c1.2-2.9-0.2-6.1,0.8-9.1c0.3-6.7,0.8-13.4,0-20.1 | ||
| 119 | c-0.5-2.3-0.5-4.6,0-6.9c1.4-10.1,7.5-16.2,19.2-19.2c2.3-0.6,4.6-0.5,6.9,0c14.1,3.9,18.5,9.8,20.2,27.2c0.5,5.3,0.5,10.6,0,15.9 | ||
| 120 | c-0.1,13.8-2.6,27.3-5.9,40.7c-4.2,16.9-13.1,30.9-24.3,43.9c-13.9,16.2-32.2,25.1-52.1,31.1c-1.9,0.6-3.9,0.4-5.9,0.7 | ||
| 121 | C1582,484.9,1580.2,484,1578.5,484.9z"/> | ||
| 122 | <path class="st0" d="M1865.9,857.9c-2,3.9-4.8,7-9.2,8.2c-2,0.4-3.9,0.5-5.9-0.1c-5.5-1.3-8.9-5.4-8.9-11.3c0-38.1,0-76.3,0-114.4 | ||
| 123 | c0-18,0.1-36,0-54c0-6.2,2.3-11.1,7.9-13.7c6.5-3,14-2,18.3,3.2c7.9,9.8,15.2,20,22.7,30.2c28,38,56,76,84,113.9 | ||
| 124 | c3.2-0.8,1.5-3.3,1.5-4.9c0.1-27.1,0-54.2,0.1-81.3c0.1-17.9-0.4-35.9,0.3-53.8c1.1-6.9,8.2-9.3,13-8.9c6,0.5,10.6,5.2,11.1,11.4 | ||
| 125 | c0.1,1,0,2,0,3c0,54.9,0,109.9,0,164.8c0,8.6-2.7,12.5-11,15.7c-2.3,0.5-4.6,0.6-6.9,0c-4.3-0.2-6.9-3-9.2-6.1 | ||
| 126 | c-27.5-37.7-54.9-75.4-82.4-113.1c-7.9-10.9-15.8-21.9-23.7-32.8c-2.7,1.1-1.2,3.5-1.2,5.1c-0.1,44.4-0.1,88.8-0.1,133.3 | ||
| 127 | C1866.2,854.3,1866.5,856.2,1865.9,857.9z"/> | ||
| 128 | <path class="st0" d="M823.8,826c-0.1,8-0.1,16-0.2,24c0,8.3-3.1,13.2-9.4,15.4c-6.9,2.4-13,0.3-17.8-6.4 | ||
| 129 | c-28.9-39.5-57.7-79.1-86.5-118.6c-6.6-9-13.1-18-19.9-27.3c-1.6,1.8-1,3.7-1,5.3c0,44.8,0,89.6,0,134.4c0,8.4-4.2,13.1-11.5,13.2 | ||
| 130 | c-6.9,0.1-11.8-4.1-12.5-11c-0.2-1.6-0.1-3.3-0.1-5c0-53.3,0.1-106.6-0.1-159.9c0-7,1.7-12.8,7.2-17.1c8.5-3.9,16.9-0.9,22.1,6.3 | ||
| 131 | c15.8,22.1,32.1,43.8,48.3,65.7c0.3,0.4,0.7,0.7,1.1,1c13.5,14.7,24.3,31.6,36.3,47.5c6,7.9,12,15.8,18.1,23.9 | ||
| 132 | c0.3-7.7,1.2-15.2,1.3-22.7c0.2-36.4,0.1-72.9,0.1-109.3c0-1.8-0.3-3.7,0.3-5.5c2-6.5,6.6-9.7,12.9-8.8c7.1,1,11.2,5.5,11.2,12.8 | ||
| 133 | c0.1,21.7,0,43.3,0,65C823.7,774.7,823.8,800.3,823.8,826z"/> | ||
| 134 | <path class="st0" d="M1069.7,768.2c0,0.8,0,1.7,0,2.5c-1.1,3,0.2,6.2-0.8,9.2c-2.6,18.1-8.2,34.9-19.9,49.5 | ||
| 135 | c-14.9,18.6-34.3,29.3-57.5,33.5c-2.9,0.5-5.9,0.8-8.9,1.3c-3,0.9-6.1-0.4-9.1,0.7c-17-0.1-33.9-0.5-50.9,0c-8,0.3-16.9-7-17.1-15 | ||
| 136 | c-0.5-1.3-0.3-2.6-0.3-4c0-51.3,0-102.6,0-154c0-1.3-0.2-2.7,0.3-4c0.5-9.7,9.7-15.4,18.1-15.2c17,0.4,34-0.2,50.9,0.2 | ||
| 137 | c25.4,0.6,48.2,8.5,66.7,26.4c14.7,14.2,23.3,31.9,26.5,52.2c0.3,2.1,0.8,4.2,1.1,6.3C1069.8,761.4,1068.6,764.9,1069.7,768.2z"/> | ||
| 138 | <path class="st0" d="M1149.6,822.9c-4.6,11.4-9.3,22.8-13.7,34.2c-1.7,4.5-4.5,7.6-9.1,9c-2,0.5-3.9,0.5-5.9,0 | ||
| 139 | c-10.6-2.3-12.1-10.7-8.7-19.4c10-25.8,20.6-51.3,30.9-77c10.4-25.8,20.5-51.7,30.9-77.5c2.1-5.1,3.8-10.3,7.8-14.7 | ||
| 140 | c10.1-11.2,29.3-6.7,35,7.5c20.4,51.3,41.2,102.5,61.9,153.7c1,2.5,2,4.9,3.1,7.3c4.1,9.4,1.5,15.9-8,20c-2,0.5-3.9,0.5-5.9,0 | ||
| 141 | c-7.8-0.6-8.7-7.3-10.9-12.5c-4.3-10.1-8.2-20.4-12.2-30.6c-1-0.8-2.2-0.5-3.4-0.5c-29.4,0-58.9,0-88.3,0 | ||
| 142 | C1151.8,822.5,1150.6,822.3,1149.6,822.9z"/> | ||
| 143 | <path class="st0" d="M1388.9,695c-0.1,2-0.3,4-0.3,6c0,50.2,0,100.5,0,150.7c0,6.8-2.3,11.9-9,14.4c-2,0.6-3.9,0.5-5.9,0.1 | ||
| 144 | c-5-0.6-7.9-3.6-9.2-8.3c-0.7-1.8-0.3-3.6-0.3-5.4c0-50.7,0-101.4,0-152.1c0-1.8,0.5-3.7-0.5-5.4l0,0c-1.7-1-3.6-0.5-5.3-0.5 | ||
| 145 | c-16.2-0.2-32.4,0.4-48.6-0.3c-5.9-1.4-8.4-5.5-7.9-12.8c0.4-4.8,3.9-8.2,9.4-8.3c9.2-0.2,18.3-0.1,27.5-0.1c33.3,0,66.6,0,99.9,0 | ||
| 146 | c5.9,0,10.3,2.1,12.2,8.1c0.4,2,0.5,3.9-0.1,5.9c-1.5,3.3-3.9,5.7-7.2,7.2c-15,0.8-29.9,0.1-44.9,0.4 | ||
| 147 | C1395.5,694.6,1392.2,693.9,1388.9,695z"/> | ||
| 148 | <path class="st7" d="M1510.8,866.1c-5-1.2-8.8-5.6-8.8-10.4c0-57.3,0.1-114.7-0.1-172c-0.1-11.6,11.1-15,18.7-10.8 | ||
| 149 | c5.4,3,5.9,8.1,5.9,13.3c0.1,14.2,0,28.3,0,42.5c0,40.8,0,81.7,0,122.5c0,8.2-2.5,11.9-9.8,14.9 | ||
| 150 | C1514.7,866.6,1512.7,866.7,1510.8,866.1z"/> | ||
| 151 | <path class="st8" d="M799.5,680.1c0,47,0,93.9,0,141.5c-2.6-1.3-3.5-3.6-4.8-5.3c-16.7-22.6-33.4-45.3-50-68 | ||
| 152 | c-0.5-0.7-0.8-1.4-1.2-2.2c1.9,0.5,2.9,2.1,4.2,3.4c15.4,15.4,30.8,30.8,46.2,46.2c1.3,1.3,2.2,2.9,3.3,4.3c0.5-0.2,1-0.5,1.5-0.7 | ||
| 153 | c0-1.7,0-3.4,0-5.1c0-36.1,0-72.2,0-108.3C798.8,683.9,798.1,681.8,799.5,680.1z"/> | ||
| 154 | <path class="st8" d="M1705.8,104.2c12.2,0,24.3,0.1,36.5-0.1c3.4-0.1,4.5,0.9,4.5,4.4c-0.2,53.8-0.2,107.6-0.2,161.4 | ||
| 155 | c0,37.8-0.1,75.6-0.2,113.5c0,2,0.4,4-0.6,5.9c0-48.5-0.1-97-0.1-145.5c0-44.3-0.1-88.7,0.1-133c0-4.5-1-6.1-5.8-5.9 | ||
| 156 | c-9.8,0.4-19.7,0.1-29.5,0.1C1709,105,1707.2,105.5,1705.8,104.2z"/> | ||
| 157 | <path class="st8" d="M1323.8,422c-1.1-1.1-0.7-2.5-0.7-3.8c-0.1-82.7-0.2-165.4-0.3-248.2c0-0.7,0.1-1.3,0.1-2 | ||
| 158 | c1.5,1.8,0.8,3.8,0.8,5.8C1323.8,256.5,1323.8,339.3,1323.8,422z"/> | ||
| 159 | <path class="st8" d="M1363.7,695c1.4,1.8,0.7,3.8,0.7,5.8c0.1,52.4,0.1,104.8,0.1,157.2c-1.5-1.7-0.8-3.8-0.8-5.7 | ||
| 160 | C1363.7,799.8,1363.7,747.4,1363.7,695z"/> | ||
| 161 | <path class="st8" d="M905.6,688.1c0,54,0,107.9,0,161.9c-1.4-1.6-0.8-3.5-0.8-5.3c0-50.5,0-100.9,0-151.4 | ||
| 162 | C904.8,691.6,904.2,689.6,905.6,688.1z"/> | ||
| 163 | <path class="st8" d="M1865.9,857.9c0-46.5,0-93,0.1-139.5c0-1.6-1.1-3.6,0.8-5c0.1,0,0.3,0.1,0.4,0.2c0.1,0.1,0.2,0.3,0.3,0.4 | ||
| 164 | c-1.4,1.8-0.8,3.8-0.8,5.8c0,44.1,0,88.2-0.1,132.3C1866.7,854.1,1867.4,856.2,1865.9,857.9z"/> | ||
| 165 | <path class="st8" d="M1976.6,680.1c0,45.9,0,91.8-0.1,137.7c0,0.5,1.9,4-1.8,2.3c1.8-1.6,1-3.7,1-5.6c0-42.9,0-85.7,0.1-128.6 | ||
| 166 | C1975.8,683.9,1975.1,681.8,1976.6,680.1z"/> | ||
| 167 | <path class="st8" d="M1792.8,409.9c-1.4-1.2-0.8-2.8-0.8-4.2c0-31.9,0-63.8,0.1-95.6c0-1.5-0.3-3,0.6-4.4 | ||
| 168 | C1792.7,340.5,1792.7,375.2,1792.8,409.9z"/> | ||
| 169 | <path class="st8" d="M1149.6,822.9c1.2-1.3,2.8-0.9,4.2-0.9c28.9,0,57.9,0,86.8,0c1.4,0,3-0.7,4.1,0.9 | ||
| 170 | C1213,823,1181.3,823,1149.6,822.9z"/> | ||
| 171 | <path class="st8" d="M1927.8,378.8c1.3,4.7,0.5,9.6,0.6,14.4c0.2,16.1,0.2,32.3,0.1,48.4c0,1.5,1.3,3.9-1.7,4.3 | ||
| 172 | c1.3-1.9,1-4.2,1-6.3C1927.8,419.3,1927.8,399,1927.8,378.8z"/> | ||
| 173 | <path class="st8" d="M1275.8,124.1c1.2,0.6,0.9,1.7,0.9,2.6c0,21.1,0,42.2-0.1,63.3c-1.4-1.6-0.8-3.5-0.8-5.2 | ||
| 174 | C1275.7,164.5,1275.7,144.3,1275.8,124.1z"/> | ||
| 175 | <path class="st8" d="M1388.9,695c1.5-1.6,3.4-0.8,5.1-0.8c16.5-0.1,33.1-0.1,49.6-0.1c-1.5,1.5-3.5,0.8-5.2,0.8 | ||
| 176 | C1422,695,1405.5,695,1388.9,695z"/> | ||
| 177 | <path class="st8" d="M1309.8,694.2c16.6,0,33.2,0,49.7,0c1.4,0,3-0.6,4.2,0.8c-16.2,0-32.5,0-48.7,0 | ||
| 178 | C1313.3,695,1311.4,695.6,1309.8,694.2z"/> | ||
| 179 | <path class="st8" d="M1925.8,65.9c0.9,0.7,0.8,1.8,0.8,2.7c0,14.8,0,29.5,0,44.3c-1.2-1.2-0.8-2.8-0.8-4.2 | ||
| 180 | C1925.8,94.4,1925.8,80.2,1925.8,65.9z"/> | ||
| 181 | <path class="st8" d="M1606.6,144c0,13.9,0,27.9,0.1,41.8c0,0.2-0.1,0.4-0.2,0.4c-0.2,0.1-0.5,0.1-0.8,0.1c0-12.4,0-24.7,0.1-37.1 | ||
| 182 | C1605.8,147.5,1605.2,145.6,1606.6,144z"/> | ||
| 183 | <path class="st8" d="M1793,105c0.9-1.5,2.4-0.8,3.6-0.8c12,0,24.1,0,36.1,0c-1.6,1.4-3.5,0.8-5.3,0.8C1816,105,1804.5,105,1793,105 | ||
| 184 | z"/> | ||
| 185 | <path class="st8" d="M1276.5,423c-6.7,1.6-13.5,0.6-20.3,0.8c-5.8,0.2-11.6,0-17.4,0c2.3-1.6,4.8-0.7,7.3-0.8 | ||
| 186 | C1256.2,422.9,1266.4,423,1276.5,423z"/> | ||
| 187 | <path class="st8" d="M1479.7,356.3c-1.2-11.8-1.1-23.7,0-35.5C1479.7,332.6,1479.7,344.4,1479.7,356.3z"/> | ||
| 188 | <path class="st8" d="M1433.6,286c-0.1,11.4,0.6,22.8-0.8,34.2c0-10.1-0.1-20.3-0.1-30.4C1432.8,288.5,1432.5,287.1,1433.6,286z"/> | ||
| 189 | <path class="st8" d="M1266.5,469.9c11-1.3,22.1-0.7,33.2-0.7c-3.8,1.8-7.8,0.7-11.7,0.8C1280.8,470.1,1273.7,470,1266.5,469.9z"/> | ||
| 190 | <path class="st8" d="M1332.5,468.9c11-1.3,22.1-0.7,33.2-0.7c-3.8,1.7-7.8,0.7-11.7,0.8C1346.8,469.1,1339.7,469,1332.5,468.9z"/> | ||
| 191 | <path class="st8" d="M1758.8,456.2c10.7,0,21.5-0.6,32.2,0.7c-9.5,0-18.9,0.1-28.4,0.1C1761.3,457,1759.9,457.3,1758.8,456.2z"/> | ||
| 192 | <path class="st8" d="M1926.8,192.8c0.8,10.4,1.4,20.7-0.1,31.1C1926.8,213.5,1926.8,203.1,1926.8,192.8z"/> | ||
| 193 | <path class="st8" d="M1823.7,410.8c-10.3-0.2-20.6,0.9-30.9-0.9c8.7,0,17.5,0,26.2,0C1820.6,410,1822.3,409.5,1823.7,410.8z"/> | ||
| 194 | <path class="st8" d="M1535.8,485.2c10,0,19.9,0,29.9,0c-4.8,1.8-9.8,0.7-14.7,0.7C1545.9,485.9,1540.8,486.9,1535.8,485.2z"/> | ||
| 195 | <path class="st8" d="M1970,18.1c-9.7,1.3-19.5,0.7-29.2,0.7c5.2-1.9,10.5-0.5,15.8-0.8C1961,17.8,1965.5,18.1,1970,18.1z"/> | ||
| 196 | <path class="st8" d="M1745.6,409c-4.1,1.7-8.4,0.5-12.5,0.7c-4.4,0.2-8.9,0.1-13.3,0.2c5.1-2,10.3-0.5,15.5-0.8 | ||
| 197 | C1738.7,408.8,1742.2,409,1745.6,409z"/> | ||
| 198 | <path class="st8" d="M1912,19.1c-8.4,1.3-16.8,0.7-25.2,0.7C1895.2,18.1,1903.6,19.4,1912,19.1z"/> | ||
| 199 | <path class="st8" d="M1432.8,356.8c1.4,7.7,0.7,15.4,0.8,23.2C1431.8,372.3,1433.1,364.5,1432.8,356.8z"/> | ||
| 200 | <path class="st8" d="M1292,77.1c-7.4,1.3-14.8,0.7-22.2,0.8C1277.1,76,1284.6,77.4,1292,77.1z"/> | ||
| 201 | <path class="st8" d="M1248,78.1c-7,1.3-14.1,0.7-21.2,0.7C1233.8,77,1241,78.4,1248,78.1z"/> | ||
| 202 | <path class="st8" d="M1205,79.1c-7,1.3-14.1,0.7-21.2,0.7C1190.8,78,1198,79.4,1205,79.1z"/> | ||
| 203 | <path class="st8" d="M1923.5,271.9c2.6-1.4,4.3-1.5,4.2,2.3c-0.2,4.9-0.1,9.8-0.1,14.7c-1.8-4.3-0.5-8.8-0.8-13.3 | ||
| 204 | C1926.7,273.3,1926.2,271.8,1923.5,271.9z"/> | ||
| 205 | <path class="st8" d="M1332,76.1c-6.3,1.4-12.8,0.7-19.2,0.7C1319.1,75.1,1325.6,76.4,1332,76.1z"/> | ||
| 206 | <path class="st8" d="M1327.5,122.9c5.7-1.3,11.4-0.7,17.2-0.7C1339,123.9,1333.3,122.6,1327.5,122.9z"/> | ||
| 207 | <path class="st8" d="M1247.5,124.9c5.3-1.3,10.8-0.7,16.2-0.7C1258.4,125.9,1252.9,124.7,1247.5,124.9z"/> | ||
| 208 | <path class="st8" d="M1671.9,367.9c0-5.3,0-10.6,0-15.9C1673.2,357.4,1673.2,362.6,1671.9,367.9z"/> | ||
| 209 | <path class="st8" d="M1215.4,125.9c5-1.3,10.2-0.7,15.3-0.7C1225.7,126.9,1220.5,125.6,1215.4,125.9z"/> | ||
| 210 | <path class="st8" d="M1338,422.1c-4.7,0.8-9.5,1.3-14.2-0.1C1328.5,422,1333.3,422,1338,422.1z"/> | ||
| 211 | <path class="st8" d="M1911.5,65.9c4.8-0.8,9.6-1.4,14.3,0C1921,65.9,1916.3,65.9,1911.5,65.9z"/> | ||
| 212 | <path class="st8" d="M1528.7,156.8c-4.6,0-9.3,0-13.9,0C1519.4,155.5,1524.1,155.5,1528.7,156.8z"/> | ||
| 213 | <path class="st8" d="M1395.8,137.8c1.2,4.3,0.6,8.8,0.7,13.2C1394.8,146.6,1396.1,142.2,1395.8,137.8z"/> | ||
| 214 | <path class="st8" d="M1361.5,121.9c4.3-1.3,8.8-0.7,13.2-0.7C1370.4,122.9,1365.9,121.6,1361.5,121.9z"/> | ||
| 215 | <path class="st8" d="M1361,75.1c-4.3,1.3-8.8,0.7-13.2,0.7C1352.1,74.1,1356.6,75.4,1361,75.1z"/> | ||
| 216 | <path class="st8" d="M813.7,382.3c-1.3-4-0.7-8.1-0.7-12.2C814.6,374,813.4,378.2,813.7,382.3z"/> | ||
| 217 | <path class="st8" d="M880.7,313.8c-4,0-7.9,0-11.9,0C872.8,312.5,876.7,312.5,880.7,313.8z"/> | ||
| 218 | <path class="st8" d="M1690.7,669.8c-3.6,0-7.3,0-10.9,0C1683.4,668.5,1687.1,668.5,1690.7,669.8z"/> | ||
| 219 | <path class="st8" d="M1384,74.1c-3.7,1.3-7.4,0.8-11.2,0.7C1376.4,73.1,1380.2,74.4,1384,74.1z"/> | ||
| 220 | <path class="st8" d="M1822.8,457.2c3.6,0,7.3,0,10.9,0C1830.1,458.5,1826.4,458.5,1822.8,457.2z"/> | ||
| 221 | <path class="st8" d="M1589.8,769.7c1.3,3.3,0.7,6.8,0.8,10.2C1588.9,776.7,1590,773.1,1589.8,769.7z"/> | ||
| 222 | <path class="st8" d="M1441.9,112.9c0-3.4-0.6-6.9,0.7-10.2C1442.5,106.2,1443.6,109.7,1441.9,112.9z"/> | ||
| 223 | <path class="st8" d="M2009.7,17.8c-3.3,0-6.6,0-9.9,0C2003.1,16.5,2006.4,16.5,2009.7,17.8z"/> | ||
| 224 | <path class="st8" d="M1853.9,439c0-3.3,0-6.6,0-9.9C1855.3,432.3,1855.3,435.7,1853.9,439z"/> | ||
| 225 | <path class="st8" d="M1069.7,768.2c-1.3-3.3-0.7-6.8-0.8-10.2C1070.6,761.3,1069.4,764.8,1069.7,768.2z"/> | ||
| 226 | <path class="st8" d="M1590.6,758.1c-0.1,3.4,0.6,6.9-0.7,10.2C1590,764.8,1588.9,761.3,1590.6,758.1z"/> | ||
| 227 | <path class="st8" d="M900.8,361.2c3.1,0,6.2-0.5,9.2,0.7C906.9,361.7,903.8,362.8,900.8,361.2z"/> | ||
| 228 | <path class="st8" d="M919.8,362.2c3.1,0,6.2-0.5,9.2,0.7C925.9,362.7,922.7,363.8,919.8,362.2z"/> | ||
| 229 | <path class="st8" d="M937.8,363.2c3.1,0,6.2-0.5,9.2,0.7C943.9,363.7,940.8,364.8,937.8,363.2z"/> | ||
| 230 | <path class="st8" d="M1385.5,120.9c3.1-0.8,6.1-1.7,9.1,0.1C1391.6,121,1388.5,120.9,1385.5,120.9z"/> | ||
| 231 | <path class="st8" d="M1402,73.1c-3,1.3-6.1,0.8-9.2,0.7C1395.8,72.2,1398.9,73.3,1402,73.1z"/> | ||
| 232 | <path class="st8" d="M1625.6,371c0,3,0.7,6.2-0.8,9.1C1625.1,377.1,1623.9,373.9,1625.6,371z"/> | ||
| 233 | <path class="st8" d="M882.8,360.2c3.1,0,6.2-0.5,9.2,0.7C888.9,360.7,885.8,361.8,882.8,360.2z"/> | ||
| 234 | <path class="st8" d="M765.8,359.8c1.2,3,0.8,6.1,0.8,9.2C764.9,366,766.1,362.8,765.8,359.8z"/> | ||
| 235 | <path class="st8" d="M1196.9,308.9c0-3,0-5.9,0-8.9C1198.2,303,1198.2,306,1196.9,308.9z"/> | ||
| 236 | <path class="st8" d="M1180.8,224.2c3,0,5.9,0,8.9,0.1C1186.7,225.4,1183.8,225.5,1180.8,224.2z"/> | ||
| 237 | <path class="st8" d="M850.5,332.1c0,3,0,5.9,0,8.9C849.3,338,849.3,335,850.5,332.1z"/> | ||
| 238 | <path class="st8" d="M1068.9,779.9c0.1-3.1-0.5-6.2,0.8-9.2C1069.4,773.8,1070.6,777,1068.9,779.9z"/> | ||
| 239 | <path class="st8" d="M973.6,864.9c3-1.3,6.1-0.7,9.1-0.7C979.8,865.8,976.6,864.7,973.6,864.9z"/> | ||
| 240 | <path class="st8" d="M1391.9,448.9c0-2.6,0-5.3,0-7.9C1393.3,443.7,1393.2,446.3,1391.9,448.9z"/> | ||
| 241 | <path class="st8" d="M1206.6,443.1c0,2.6,0,5.3,0,7.9C1205.3,448.3,1205.3,445.7,1206.6,443.1z"/> | ||
| 242 | <path class="st8" d="M766.8,381.7c1.2,2.7,0.7,5.5,0.7,8.2C765.9,387.3,767.1,384.5,766.8,381.7z"/> | ||
| 243 | <path class="st8" d="M850,464.1c-2.8,1.4-5.6,1.2-8.4,0C844.4,464.1,847.2,464.1,850,464.1z"/> | ||
| 244 | <path class="st8" d="M1442.7,146.2c-1.3-2.6-0.8-5.4-0.8-8.1C1443.6,140.6,1442.4,143.5,1442.7,146.2z"/> | ||
| 245 | <path class="st8" d="M1651.7,324.8c-2.3,0-4.6,0-6.9,0C1647.1,323.5,1649.4,323.6,1651.7,324.8z"/> | ||
| 246 | <path class="st8" d="M895.7,314.8c-2.4,0-4.9,0.4-7.2-0.7C890.9,314.3,893.4,313.2,895.7,314.8z"/> | ||
| 247 | <path class="st8" d="M1417,72.1c-2.3,1.2-4.8,0.8-7.2,0.7C1412.1,71.2,1414.6,72.3,1417,72.1z"/> | ||
| 248 | <path class="st8" d="M1415.8,175.2c2.3,0,4.6,0,6.9,0C1420.4,176.4,1418.1,176.4,1415.8,175.2z"/> | ||
| 249 | <path class="st8" d="M812.7,360.3c-1.2-2.3-0.7-4.8-0.7-7.2C813.5,355.3,812.5,357.9,812.7,360.3z"/> | ||
| 250 | <path class="st8" d="M973.9,343.9c0-2.3,0-4.6,0-6.9C975.2,339.4,975.2,341.7,973.9,343.9z"/> | ||
| 251 | <path class="st8" d="M1625.6,344c0,2.3,0,4.6,0,6.9C1624.3,348.6,1624.3,346.3,1625.6,344z"/> | ||
| 252 | <path class="st8" d="M1578,438.1c-2.3,1.3-4.8,0.8-7.2,0.7C1573.1,437.2,1575.6,438.3,1578,438.1z"/> | ||
| 253 | <path class="st8" d="M1982.8,866.1c2.3,0,4.6,0,6.9,0C1987.4,867.5,1985.1,867.5,1982.8,866.1z"/> | ||
| 254 | <path class="st8" d="M1479.9,281c-0.1-2.1-0.4-4.2,0.7-6.2C1480.5,276.8,1481.4,279,1479.9,281z"/> | ||
| 255 | <path class="st8" d="M1632.7,123.8c-2,0-3.9,0-5.9,0C1628.8,122.6,1630.7,122.5,1632.7,123.8z"/> | ||
| 256 | <path class="st8" d="M954.7,317.8c-2,0-5,0.9-3.1-3.1C951.5,316.9,953.7,316.8,954.7,317.8z"/> | ||
| 257 | <path class="st8" d="M1994.5,483.9c2-1.1,4.1-0.8,6.2-0.7C1998.8,484.7,1996.6,483.8,1994.5,483.9z"/> | ||
| 258 | <path class="st8" d="M764.8,347.7c1.2,2,0.8,4.1,0.7,6.2C764,352,765,349.8,764.8,347.7z"/> | ||
| 259 | <path class="st8" d="M718.9,417.9c0-2,0-3.9,0-5.9C720.2,414,720.2,416,718.9,417.9z"/> | ||
| 260 | <path class="st8" d="M1227.8,470.2c2,0,3.9,0,5.9,0C1231.7,471.4,1229.8,471.4,1227.8,470.2z"/> | ||
| 261 | <path class="st8" d="M1780.7,763.3c-1.2-2-0.7-4.1-0.7-6.2C1781.4,759,1780.5,761.2,1780.7,763.3z"/> | ||
| 262 | <path class="st8" d="M1450.9,687c0-2,0-3.9,0.1-5.9C1452.2,683,1452.2,685,1450.9,687z"/> | ||
| 263 | <path class="st8" d="M1982.5,484.9c2-1.1,4.1-0.8,6.2-0.8C1986.8,485.7,1984.6,484.8,1982.5,484.9z"/> | ||
| 264 | <path class="st8" d="M1779.9,780.9c0-2.1-0.5-4.2,0.7-6.2C1780.5,776.8,1781.5,779,1779.9,780.9z"/> | ||
| 265 | <path class="st8" d="M767.8,395.8c1.1,1.9,0.8,4.1,0.8,6.2C767,400.1,768,397.9,767.8,395.8z"/> | ||
| 266 | <path class="st8" d="M1510.8,866.1c2,0,3.9,0.1,5.9,0.1C1514.7,867.4,1512.7,867.5,1510.8,866.1z"/> | ||
| 267 | <path class="st8" d="M1120.8,866.2c2,0,3.9,0,5.9,0C1124.7,867.5,1122.8,867.5,1120.8,866.2z"/> | ||
| 268 | <path class="st8" d="M1267.8,866.1c2,0,3.9,0,5.9,0C1271.7,867.5,1269.8,867.5,1267.8,866.1z"/> | ||
| 269 | <path class="st8" d="M1373.8,866.2c2,0,3.9-0.1,5.9-0.1C1377.8,867.5,1375.8,867.4,1373.8,866.2z"/> | ||
| 270 | <path class="st8" d="M1850.8,866.1c2,0,3.9,0,5.9,0.1C1854.7,867.4,1852.7,867.5,1850.8,866.1z"/> | ||
| 271 | <path class="st8" d="M868.8,359.2c1.8,0,3.5-0.2,5.2,0.7C872.2,359.8,870.4,360.6,868.8,359.2z"/> | ||
| 272 | <path class="st8" d="M1394.6,125.1c0,1.6,0,3.3,0,4.9C1393.3,128.3,1393.4,126.7,1394.6,125.1z"/> | ||
| 273 | <path class="st8" d="M763.8,337.7c1.1,1.6,0.9,3.4,0.8,5.2C763.1,341.4,763.9,339.5,763.8,337.7z"/> | ||
| 274 | <path class="st8" d="M811.7,346.3c-1-1.6-0.8-3.5-0.7-5.2C812.3,342.6,811.6,344.5,811.7,346.3z"/> | ||
| 275 | <path class="st8" d="M1972.5,485.9c1.6-1,3.4-0.8,5.2-0.7C1976.1,486.6,1974.3,485.8,1972.5,485.9z"/> | ||
| 276 | <path class="st8" d="M1673.9,670.1c-1.6,1.1-3.4,0.9-5.1,0.8C1670.3,669.4,1672.2,670.2,1673.9,670.1z"/> | ||
| 277 | <path class="st8" d="M814.7,395.3c-1.1-1.6-0.8-3.4-0.7-5.2C815.3,391.6,814.6,393.5,814.7,395.3z"/> | ||
| 278 | <path class="st8" d="M2007.5,482.9c1.6-1,3.4-0.8,5.2-0.8C2011.2,483.6,2009.3,482.8,2007.5,482.9z"/> | ||
| 279 | <path class="st8" d="M810.7,336.3c-1.1-1.6-0.8-3.4-0.8-5.2C811.4,332.6,810.6,334.5,810.7,336.3z"/> | ||
| 280 | <path class="st8" d="M1434.6,268.1c0.1,1.8,0.3,3.6-0.7,5.3C1433.9,271.5,1433.1,269.6,1434.6,268.1z"/> | ||
| 281 | <path class="st8" d="M1536.7,438.8c-1.7,0.1-3.5,0.3-5.1-0.8C1533.3,438.2,1535.2,437.4,1536.7,438.8z"/> | ||
| 282 | <path class="st8" d="M1578.5,484.9c1.6-1,3.4-0.8,5.1-0.7C1582.1,485.6,1580.3,484.8,1578.5,484.9z"/> | ||
| 283 | <path class="st6" d="M173.5,93.3c0-12.8-0.1-25.6,0-38.4c0.1-11.6,8-20.7,19.3-22.6c10-1.7,20.8,4.6,24.8,14.6 | ||
| 284 | c1.2,2.9,1.6,5.9,1.5,8.8c-1.3,34.8-2.8,69.6-4.1,104.3c-0.5,12.7-1,25.3-1.4,38c-0.4,13.2-9.5,22.9-21.9,23.2 | ||
| 285 | c-13.4,0.3-22.9-8.6-24-22.2c-1-12.6-2.8-25.1-7-37.2c-5.1-14.4-12.7-27.1-23.9-37.5c-6.7-6.2-12.5-13.5-20.8-17.9 | ||
| 286 | c-4.2-2.2-8.4-4.1-13.1-4.4c-14.3-0.9-21.8,4.5-26.1,18.6c-3.7,12.2-5.1,24.7-6.8,37.3c-1.9,13.4-3,26.8-2.3,40.3 | ||
| 287 | c0.3,5.4,1.2,10.6,2.5,15.7c4.9,18.4,18.3,25.5,36,28.1c30.7,4.5,61,10.8,88.6,26c24.3,13.4,42,32.2,47.9,60.1 | ||
| 288 | c7,33.1-2,62.2-24.6,86.8c-25.9,28.2-58.4,41.5-96.6,38.8c-25.5-1.8-48.9-10.9-70.8-23.9c-1.3-0.8-2.5-1.5-4.6-2.7 | ||
| 289 | c0,6.9-0.1,13.1,0,19.3c0.2,7.7,0.6,15.3,0.8,23c0.3,13.9-8.6,24.2-21.3,24.7c-13.6,0.5-24-8.8-24.6-22.7 | ||
| 290 | c-1.3-28.8-1.4-57.6,0.5-86.4c0.8-12.5,0.9-25,1.3-37.5c0.3-9.3,1-18.7,0.6-28c-0.5-12,8.8-24.4,23.9-23.9 | ||
| 291 | c13.8,0.4,22.4,12.2,22.2,26.7c-0.2,21.6,3.6,42.4,20.4,58c14.5,13.5,30.3,24.8,51,27.4c36.7,4.6,71.6-20.5,77.4-50.4 | ||
| 292 | c3.5-18.2-4.5-35.6-22.1-46.5c-16.3-10.1-34.5-14.8-52.9-18.7c-11.6-2.4-23.3-3.9-34.8-6.5c-35.5-7.9-56-30.7-63.6-65.8 | ||
| 293 | c-4.8-22.2-3.2-44.4-0.5-66.6c2-16.4,4.2-32.7,9.7-48.3c8.6-24.8,23.9-42.6,50.9-47.6c23.1-4.3,43.9,1.4,62.8,15 | ||
| 294 | c8.8,6.3,16.8,13.5,24.2,21.4C172.4,93.6,173,93.5,173.5,93.3z"/> | ||
| 295 | <path class="st6" d="M285.6,342.2c0-17-0.7-34,0.1-51c1.3-28.1,5.9-55.7,19.3-80.9c11.8-22.1,29.5-36.7,54.7-40.9 | ||
| 296 | c18.7-3.1,36.8,0.4,54.5,6.4c14.4,4.9,28.1,11.2,40.4,20.4c1,0.7,2,2.3,3.4,1.4c1.2-0.8,0.5-2.5,0.6-3.7c0-11.5,0-23,0-34.5 | ||
| 297 | c0.1-13.8,9.8-24,22.8-24c12.6,0,23,10.1,23.1,23.4c0.2,32.8,0.2,65.7,0,98.5c-0.1,11.2-6.8,19.8-17,22.6 | ||
| 298 | c-10.2,2.8-19.4-0.9-25.8-10.3c-11.3-16.5-24.6-31.2-42.1-41.2c-13.1-7.5-27.3-12.4-42.4-13.9c-12.5-1.3-22.5,3.1-29.3,14.1 | ||
| 299 | c-8.3,13.3-12,28.2-13.9,43.4c-4.6,37.6-3.2,75.5-2.5,113.3c0.2,8.7,0.9,17.3,3.8,25.5c8.9,24.9,29.1,39.5,56.3,40.3 | ||
| 300 | c12.7,0.4,25.3,0.4,38-0.9c9.9-1,18.5-5.8,25.7-12.4c11.4-10.4,18-23.5,20.7-38.9c2.6-14.4,1.8-28.9,1.7-43.3 | ||
| 301 | c-0.1-9.8,4.2-17.1,13-21.2c8.8-4.1,17.2-2.8,24.8,3.3c4.3,3.5,7,8.1,7.5,13.7c2.3,25.8,1.7,51.4-7.2,75.9 | ||
| 302 | c-13.6,37.2-39.5,62-79.1,68c-26.5,4-53.9,4.9-80.1-4.2c-38-13.2-59.5-41.2-68.4-79.4c-4.4-19.1-2.6-38.8-3.1-58.2 | ||
| 303 | c-0.1-3.7,0-7.3,0-11C285,342.2,285.3,342.2,285.6,342.2z"/> | ||
| 304 | <path class="st0" d="M7.7,771.5c0-26.3,0-52.6,0-79c0-12.8,6.6-19.5,19.3-19.5c32.2,0,64.3,0,96.5,0c8.1,0,12.5,5,11.4,12.8 | ||
| 305 | c-0.7,5.2-4.8,8.3-11.2,8.3c-24.3,0-48.6,0-73,0c-5.2,0-10.3,0.1-15.5,0c-2.6-0.1-3.5,0.9-3.5,3.5c0.1,18.3,0.1,36.6,0,55 | ||
| 306 | c0,4.2,2.5,3.5,4.9,3.5c28.3,0,56.6,0,85,0c7,0,11.1,3.6,11.4,9.9c0.3,7-3.6,11.2-10.9,11.2c-26.5,0.1-53,0-79.5,0 | ||
| 307 | c-10.8,0-10.8,0-10.8,11c0,21,0,42,0,63c0,10.2-4.1,15.1-12.4,15c-7.9-0.1-11.6-5-11.6-15.2C7.7,824.5,7.7,798,7.7,771.5z"/> | ||
| 308 | <path class="st9" d="M1755.7,768.8c-0.4,19.4-4.9,37.6-17.2,53.1c-11.9,14.8-27.3,22.8-46.3,24.4c-22.4,1.9-41.8-4.3-57.4-20.9 | ||
| 309 | c-10.8-11.6-16.5-25.7-18.7-41.2c-3-20.2-0.7-39.8,9-58.2c13.5-25.6,41.3-37.3,68.3-34.5c34.8,3.7,57.9,31.3,61.5,65.8 | ||
| 310 | C1755.3,761.2,1755.8,764.9,1755.7,768.8z"/> | ||
| 311 | <path class="st9" d="M344.7,769c-0.4,19.3-4.8,37.2-17,52.6c-11.8,14.9-27.2,23-46.1,24.7c-22.6,2-42.2-4.2-57.8-21 | ||
| 312 | c-10.8-11.6-16.4-25.8-18.7-41.2c-2.9-20.6-0.6-40.5,9.6-59c14-25.4,41.1-36.1,66.9-33.6c37,3.6,58.9,32.6,62.3,66 | ||
| 313 | C344.3,761.3,344.8,765.1,344.7,769z"/> | ||
| 314 | <path class="st9" d="M658.5,162.5c0-15.6,0.1-31.3-0.1-46.9c0-3.9,0.7-5.5,5.1-5.5c29.7-0.2,59.2,0.5,88.3,7 | ||
| 315 | c8.9,2,17.7,4.8,25.2,10.4c5.5,4.1,8.2,9.4,5.8,16.1c-7.9,22.2-19.6,41.1-43.8,48.9c-24.7,7.9-49.2,16.3-74.7,21.3 | ||
| 316 | c-4.6,0.9-5.8,0-5.7-4.8c0.3-15.5,0.1-31,0.1-46.4C658.6,162.5,658.6,162.5,658.5,162.5z"/> | ||
| 317 | <path class="st9" d="M994.1,108.4c10.2,25.2,19.9,49,29.6,72.9c6.3,15.4,12.4,30.8,18.8,46.2c1.5,3.6,1.7,5.4-3,6.1 | ||
| 318 | c-23,3.6-45.9,7.4-68.9,11.4c-4.6,0.8-4.5-1.1-3.8-4.5c8.5-41.2,16.9-82.4,25.3-123.5C992.7,114.6,993.2,112.2,994.1,108.4z"/> | ||
| 319 | <path class="st9" d="M929.7,769c0-23.3,0.1-46.6-0.1-69.9c0-3.9,0.9-5.2,5-5c15.1,0.6,30.3-0.4,45.4,0.7 | ||
| 320 | c31.9,2.5,56.8,24.7,62.7,56.2c3.9,21,2,41.2-9.4,59.7c-12.3,20-30.9,30.5-54,32.1c-15.2,1.1-30.6,0.2-45.9,0.4 | ||
| 321 | c-4,0.1-3.7-2.1-3.7-4.7C929.8,815.3,929.7,792.2,929.7,769z"/> | ||
| 322 | <path class="st9" d="M1197.5,698.8c4.5,11.3,8.8,22,13,32.8c8.5,21.5,16.8,43.1,25.4,64.5c1.6,4,1.1,4.9-3.3,4.9 | ||
| 323 | c-23.8-0.2-47.6-0.1-71.4-0.1c-2,0-5.1,1.1-3.4-3.3c12.8-32.1,25.5-64.3,38.3-96.4C1196.4,700.5,1196.8,700,1197.5,698.8z"/> | ||
| 324 | </g> | ||
| 325 | </svg> |
src/main/.eslintrc.js
0 → 100644
src/main/FileFilters.js
0 → 100644
| 1 | const saveFilters = { | ||
| 2 | JPEG: { | ||
| 3 | name: 'JPEG Image', | ||
| 4 | extensions: ['jpg', 'jpeg'] | ||
| 5 | }, | ||
| 6 | MP3: { | ||
| 7 | name: 'MP3 Sound', | ||
| 8 | extensions: ['mp3'] | ||
| 9 | }, | ||
| 10 | PNG: { | ||
| 11 | name: 'PNG Image', | ||
| 12 | extensions: ['png'] | ||
| 13 | }, | ||
| 14 | SB: { | ||
| 15 | name: 'Scratch 1 Project', | ||
| 16 | extensions: ['sb'] | ||
| 17 | }, | ||
| 18 | SB2: { | ||
| 19 | name: 'Scratch 2 Project', | ||
| 20 | extensions: ['sb2'] | ||
| 21 | }, | ||
| 22 | SB3: { | ||
| 23 | name: 'Scratch 3 Project', | ||
| 24 | extensions: ['sb3'] | ||
| 25 | }, | ||
| 26 | OB: { | ||
| 27 | name: 'OpenBlock Project', | ||
| 28 | extensions: ['ob'] | ||
| 29 | }, | ||
| 30 | Sprite2: { | ||
| 31 | name: 'Scratch 2 Sprite', | ||
| 32 | extensions: ['sprite2'] | ||
| 33 | }, | ||
| 34 | Sprite3: { | ||
| 35 | name: 'Scratch 3 Sprite', | ||
| 36 | extensions: ['sprite3'] | ||
| 37 | }, | ||
| 38 | SVG: { | ||
| 39 | name: 'SVG Image', | ||
| 40 | extensions: ['svg'] | ||
| 41 | }, | ||
| 42 | WAV: { | ||
| 43 | name: 'WAV Sound', | ||
| 44 | extensions: ['wav'] | ||
| 45 | } | ||
| 46 | }; | ||
| 47 | |||
| 48 | const loadFilters = { | ||
| 49 | ...saveFilters, | ||
| 50 | AllBitmaps: { | ||
| 51 | name: 'All Bitmaps', | ||
| 52 | extensions: [ | ||
| 53 | ...saveFilters.JPEG.extensions, | ||
| 54 | ...saveFilters.PNG.extensions | ||
| 55 | ] | ||
| 56 | }, | ||
| 57 | AllImages: { | ||
| 58 | name: 'All Images', | ||
| 59 | extensions: [ | ||
| 60 | ...saveFilters.JPEG.extensions, | ||
| 61 | ...saveFilters.PNG.extensions, | ||
| 62 | ...saveFilters.SVG.extensions | ||
| 63 | ] | ||
| 64 | }, | ||
| 65 | AllProjects: { | ||
| 66 | name: 'All OpenBlock Projects', | ||
| 67 | extensions: [ | ||
| 68 | ...saveFilters.SB3.extensions, | ||
| 69 | ...saveFilters.SB2.extensions, | ||
| 70 | ...saveFilters.SB.extensions, | ||
| 71 | ...saveFilters.OB.extensions | ||
| 72 | ] | ||
| 73 | }, | ||
| 74 | AllSounds: { | ||
| 75 | name: 'All Sounds', | ||
| 76 | extensions: [ | ||
| 77 | ...saveFilters.MP3.extensions, | ||
| 78 | ...saveFilters.WAV.extensions | ||
| 79 | ] | ||
| 80 | }, | ||
| 81 | AllSprites: { | ||
| 82 | name: 'All Sprites', | ||
| 83 | extensions: [ | ||
| 84 | ...saveFilters.Sprite3.extensions, | ||
| 85 | ...saveFilters.Sprite2.extensions | ||
| 86 | ] | ||
| 87 | } | ||
| 88 | }; | ||
| 89 | |||
| 90 | const filtersByExtension = Object.values(saveFilters).reduce((result, filter) => { | ||
| 91 | for (const extension of filter.extensions) { | ||
| 92 | result[extension] = filter; | ||
| 93 | } | ||
| 94 | return result; | ||
| 95 | }, {}); | ||
| 96 | |||
| 97 | const getFilterForExtension = extNameNoDot => | ||
| 98 | filtersByExtension[extNameNoDot] || { | ||
| 99 | name: `${extNameNoDot.toUpperCase()} Files`, | ||
| 100 | extensions: [extNameNoDot] | ||
| 101 | }; | ||
| 102 | |||
| 103 | export { | ||
| 104 | saveFilters, | ||
| 105 | loadFilters, | ||
| 106 | getFilterForExtension | ||
| 107 | }; |
src/main/MacOSMenu.js
0 → 100644
| 1 | // Include the standard keyboard shortcuts in the edit menu | ||
| 2 | // so they can be used within the app. Only needed on Mac. | ||
| 3 | export default app => ([ | ||
| 4 | { | ||
| 5 | label: 'App', // Always overridden by app name | ||
| 6 | submenu: [{ | ||
| 7 | label: 'Quit', | ||
| 8 | accelerator: 'CmdOrCtrl+Q', | ||
| 9 | click: () => app.quit() | ||
| 10 | }] | ||
| 11 | }, | ||
| 12 | { | ||
| 13 | label: 'Edit', | ||
| 14 | submenu: [ | ||
| 15 | { | ||
| 16 | label: 'Undo', | ||
| 17 | accelerator: 'CmdOrCtrl+Z', | ||
| 18 | role: 'undo' | ||
| 19 | }, | ||
| 20 | { | ||
| 21 | label: 'Redo', | ||
| 22 | accelerator: 'Shift+CmdOrCtrl+Z', | ||
| 23 | role: 'redo' | ||
| 24 | }, | ||
| 25 | { | ||
| 26 | type: 'separator' | ||
| 27 | }, | ||
| 28 | { | ||
| 29 | label: 'Cut', | ||
| 30 | accelerator: 'CmdOrCtrl+X', | ||
| 31 | role: 'cut' | ||
| 32 | }, | ||
| 33 | { | ||
| 34 | label: 'Copy', | ||
| 35 | accelerator: 'CmdOrCtrl+C', | ||
| 36 | role: 'copy' | ||
| 37 | }, | ||
| 38 | { | ||
| 39 | label: 'Paste', | ||
| 40 | accelerator: 'CmdOrCtrl+V', | ||
| 41 | role: 'paste' | ||
| 42 | }, | ||
| 43 | { | ||
| 44 | label: 'Select All', | ||
| 45 | accelerator: 'CmdOrCtrl+A', | ||
| 46 | role: 'selectall' | ||
| 47 | } | ||
| 48 | ] | ||
| 49 | } | ||
| 50 | ]); |
src/main/OpenblockDesktopLink.js
0 → 100644
| 1 | import {app} from 'electron'; | ||
| 2 | import path from 'path'; | ||
| 3 | import os from 'os'; | ||
| 4 | import {execFile, spawn} from 'child_process'; | ||
| 5 | import fs from 'fs-extra'; | ||
| 6 | |||
| 7 | import sudo from 'sudo-prompt'; | ||
| 8 | import {productName} from '../../package.json'; | ||
| 9 | |||
| 10 | import OpenBlockLink from 'openblock-link'; | ||
| 11 | import OpenblockResourceServer from 'openblock-resource'; | ||
| 12 | |||
| 13 | class OpenblockDesktopLink { | ||
| 14 | constructor () { | ||
| 15 | this._resourceServer = null; | ||
| 16 | |||
| 17 | this.appPath = app.getAppPath(); | ||
| 18 | if (this.appPath.search(/app/g) !== -1) { | ||
| 19 | // Normal app | ||
| 20 | this.appPath = path.join(this.appPath, '../../'); | ||
| 21 | } else if (this.appPath.search(/main/g) !== -1) { // eslint-disable-line no-negated-condition | ||
| 22 | // Start by start script in debug mode. | ||
| 23 | this.appPath = path.join(this.appPath, '../../'); | ||
| 24 | } else { | ||
| 25 | // App in dir mode | ||
| 26 | this.appPath = path.join(this.appPath, '../'); | ||
| 27 | } | ||
| 28 | |||
| 29 | const userDataPath = app.getPath( | ||
| 30 | 'userData' | ||
| 31 | ); | ||
| 32 | this.dataPath = path.join(userDataPath, 'Data'); | ||
| 33 | |||
| 34 | this._link = new OpenBlockLink(this.dataPath, path.join(this.appPath, 'tools')); | ||
| 35 | this._resourceServer = new OpenblockResourceServer(this.dataPath, | ||
| 36 | path.join(this.appPath, 'external-resources'), | ||
| 37 | app.getLocaleCountryCode()); | ||
| 38 | } | ||
| 39 | |||
| 40 | get resourceServer () { | ||
| 41 | return this._resourceServer; | ||
| 42 | } | ||
| 43 | |||
| 44 | installDriver (callback = null) { | ||
| 45 | const driverPath = path.join(this.appPath, 'drivers'); | ||
| 46 | if ((os.platform() === 'win32') && (os.arch() === 'x64')) { | ||
| 47 | execFile('install_x64.bat', [], {cwd: driverPath}); | ||
| 48 | } else if ((os.platform() === 'win32') && (os.arch() === 'ia32')) { | ||
| 49 | execFile('install_x86.bat', [], {cwd: driverPath}); | ||
| 50 | } else if ((os.platform() === 'darwin')) { | ||
| 51 | spawn('sh', ['install.sh'], {shell: true, cwd: driverPath}); | ||
| 52 | } else if ((os.platform() === 'linux')) { | ||
| 53 | sudo.exec(`sh ${path.join(driverPath, 'linux_setup.sh')} yang`, {name: productName}, | ||
| 54 | error => { | ||
| 55 | if (error) throw error; | ||
| 56 | if (callback) { | ||
| 57 | callback(); | ||
| 58 | } | ||
| 59 | } | ||
| 60 | ); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | clearCache (reboot = true) { | ||
| 65 | if (fs.existsSync(this.dataPath)) { | ||
| 66 | fs.rmSync(this.dataPath, {recursive: true, force: true}); | ||
| 67 | } | ||
| 68 | if (reboot){ | ||
| 69 | app.relaunch(); | ||
| 70 | app.exit(); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | start () { | ||
| 75 | this._link.listen(); | ||
| 76 | |||
| 77 | // start resource server | ||
| 78 | this._resourceServer.listen(); | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | export default OpenblockDesktopLink; |
src/main/OpenblockDesktopTelemetry.js
0 → 100644
| 1 | import {app, ipcMain} from 'electron'; | ||
| 2 | import defaultsDeep from 'lodash.defaultsdeep'; | ||
| 3 | import {version} from '../../package.json'; | ||
| 4 | |||
| 5 | import TelemetryClient from './telemetry/TelemetryClient'; | ||
| 6 | |||
| 7 | const EVENT_TEMPLATE = { | ||
| 8 | version, | ||
| 9 | projectName: '', | ||
| 10 | language: '', | ||
| 11 | metadata: { | ||
| 12 | scriptCount: -1, | ||
| 13 | spriteCount: -1, | ||
| 14 | variablesCount: -1, | ||
| 15 | blocksCount: -1, | ||
| 16 | costumesCount: -1, | ||
| 17 | listsCount: -1, | ||
| 18 | soundsCount: -1 | ||
| 19 | } | ||
| 20 | }; | ||
| 21 | |||
| 22 | const APP_ID = 'openblock-desktop'; | ||
| 23 | const APP_VERSION = app.getVersion(); | ||
| 24 | const APP_INFO = Object.freeze({ | ||
| 25 | projectName: `${APP_ID} ${APP_VERSION}` | ||
| 26 | }); | ||
| 27 | |||
| 28 | class ScratchDesktopTelemetry { | ||
| 29 | constructor () { | ||
| 30 | this._telemetryClient = new TelemetryClient(); | ||
| 31 | } | ||
| 32 | |||
| 33 | get didOptIn () { | ||
| 34 | return this._telemetryClient.didOptIn; | ||
| 35 | } | ||
| 36 | set didOptIn (value) { | ||
| 37 | this._telemetryClient.didOptIn = value; | ||
| 38 | } | ||
| 39 | |||
| 40 | appWasOpened () { | ||
| 41 | this._telemetryClient.addEvent('app::open', {...EVENT_TEMPLATE, ...APP_INFO}); | ||
| 42 | } | ||
| 43 | |||
| 44 | appWillClose () { | ||
| 45 | this._telemetryClient.addEvent('app::close', {...EVENT_TEMPLATE, ...APP_INFO}); | ||
| 46 | } | ||
| 47 | |||
| 48 | projectDidLoad (metadata = {}) { | ||
| 49 | this._telemetryClient.addEvent('project::load', this._buildMetadata(metadata)); | ||
| 50 | } | ||
| 51 | |||
| 52 | projectDidSave (metadata = {}) { | ||
| 53 | // Since the save dialog appears on the main process the GUI does not wait for the actual save to complete. | ||
| 54 | // That means the GUI sends this event before we know the file name used for the save, which is where the new | ||
| 55 | // project title comes from. Instead, just hold on to this metadata pending a `projectSaveCompleted` event | ||
| 56 | // from the save code on the main process. If the user cancels the save this data will be cleared. | ||
| 57 | this._pendingProjectSave = metadata; | ||
| 58 | } | ||
| 59 | |||
| 60 | projectSaveCompleted (newProjectTitle) { | ||
| 61 | const metadata = this._pendingProjectSave; | ||
| 62 | this._pendingProjectSave = null; | ||
| 63 | |||
| 64 | metadata.projectName = newProjectTitle; | ||
| 65 | this._telemetryClient.addEvent('project::save', this._buildMetadata(metadata)); | ||
| 66 | } | ||
| 67 | |||
| 68 | projectSaveCanceled () { | ||
| 69 | this._pendingProjectSave = null; | ||
| 70 | } | ||
| 71 | |||
| 72 | projectWasCreated (metadata = {}) { | ||
| 73 | this._telemetryClient.addEvent('project::create', this._buildMetadata(metadata)); | ||
| 74 | } | ||
| 75 | |||
| 76 | projectWasUploaded (metadata = {}) { | ||
| 77 | this._telemetryClient.addEvent('project::upload', this._buildMetadata(metadata)); | ||
| 78 | } | ||
| 79 | |||
| 80 | _buildMetadata (metadata) { | ||
| 81 | const {projectName, language, ...codeMetadata} = metadata; | ||
| 82 | return defaultsDeep({ | ||
| 83 | projectName, | ||
| 84 | language, | ||
| 85 | metadata: codeMetadata | ||
| 86 | }, EVENT_TEMPLATE); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | // make a singleton so it's easy to share across both Electron processes | ||
| 91 | const scratchDesktopTelemetrySingleton = new ScratchDesktopTelemetry(); | ||
| 92 | |||
| 93 | // `handle` works with `invoke` | ||
| 94 | ipcMain.handle('getTelemetryDidOptIn', () => | ||
| 95 | scratchDesktopTelemetrySingleton.didOptIn | ||
| 96 | ); | ||
| 97 | // `on` works with `sendSync` (and `send`) | ||
| 98 | ipcMain.on('getTelemetryDidOptIn', event => { | ||
| 99 | event.returnValue = scratchDesktopTelemetrySingleton.didOptIn; | ||
| 100 | }); | ||
| 101 | ipcMain.on('setTelemetryDidOptIn', (event, arg) => { | ||
| 102 | scratchDesktopTelemetrySingleton.didOptIn = arg; | ||
| 103 | }); | ||
| 104 | ipcMain.on('projectDidLoad', (event, arg) => { | ||
| 105 | scratchDesktopTelemetrySingleton.projectDidLoad(arg); | ||
| 106 | }); | ||
| 107 | ipcMain.on('projectDidSave', (event, arg) => { | ||
| 108 | scratchDesktopTelemetrySingleton.projectDidSave(arg); | ||
| 109 | }); | ||
| 110 | ipcMain.on('projectWasCreated', (event, arg) => { | ||
| 111 | scratchDesktopTelemetrySingleton.projectWasCreated(arg); | ||
| 112 | }); | ||
| 113 | ipcMain.on('projectWasUploaded', (event, arg) => { | ||
| 114 | scratchDesktopTelemetrySingleton.projectWasUploaded(arg); | ||
| 115 | }); | ||
| 116 | |||
| 117 | export default scratchDesktopTelemetrySingleton; |
src/main/OpenblockDesktopUpdater.js
0 → 100644
| 1 | import {app} from 'electron'; | ||
| 2 | import {autoUpdater, CancellationToken} from 'electron-updater'; | ||
| 3 | import log from 'electron-log'; | ||
| 4 | import bytes from 'bytes'; | ||
| 5 | import path from 'path'; | ||
| 6 | import fetch from 'electron-fetch'; | ||
| 7 | |||
| 8 | import formatMessage from 'format-message'; | ||
| 9 | import parseReleaseMessage from 'openblock-parse-release-message'; | ||
| 10 | import {UPDATE_TARGET, UPDATE_MODAL_STATE} from 'openblock-gui/src/lib/update-state.js'; | ||
| 11 | import {AbortController} from 'node-abort-controller'; | ||
| 12 | class OpenblockDesktopUpdater { | ||
| 13 | constructor (webContents, resourceServer) { | ||
| 14 | this._webContents = webContents; | ||
| 15 | this._resourceServer = resourceServer; | ||
| 16 | |||
| 17 | autoUpdater.autoDownload = false; | ||
| 18 | |||
| 19 | const appPath = app.getAppPath(); | ||
| 20 | if (appPath.search(/main/g) !== -1) { | ||
| 21 | autoUpdater.logger = log; | ||
| 22 | autoUpdater.logger.transports.file.level = 'info'; | ||
| 23 | autoUpdater.updateConfigPath = path.join(appPath, '../win-unpacked/resources/app-update.yml'); | ||
| 24 | } | ||
| 25 | |||
| 26 | this.updaterState = null; | ||
| 27 | this.updateTarget = null; | ||
| 28 | this.abortController = null; | ||
| 29 | this.cancellationToken = null; | ||
| 30 | } | ||
| 31 | |||
| 32 | removeAllAutoUpdaterListeners () { | ||
| 33 | autoUpdater.removeAllListeners('error'); | ||
| 34 | autoUpdater.removeAllListeners('update-available'); | ||
| 35 | autoUpdater.removeAllListeners('update-not-available'); | ||
| 36 | } | ||
| 37 | |||
| 38 | reportUpdateState (state) { | ||
| 39 | this._webContents.send('setUpdate', state); | ||
| 40 | } | ||
| 41 | |||
| 42 | applicationAvailable (info) { | ||
| 43 | this.updateTarget = UPDATE_TARGET.application; | ||
| 44 | |||
| 45 | if (this.isCN) { | ||
| 46 | const url = `https://openblock.sgp1.digitaloceanspaces.com/desktop/latestRelease.json`; | ||
| 47 | |||
| 48 | fetch(url) | ||
| 49 | .then(res => res.json()) | ||
| 50 | .then(data => { | ||
| 51 | this.reportUpdateState({ | ||
| 52 | phase: UPDATE_MODAL_STATE.applicationUpdateAvailable, | ||
| 53 | info: { | ||
| 54 | version: info.version, | ||
| 55 | message: parseReleaseMessage(data.body) | ||
| 56 | } | ||
| 57 | }); | ||
| 58 | }) | ||
| 59 | .catch(err => { | ||
| 60 | this.reportUpdateState({ | ||
| 61 | phase: UPDATE_MODAL_STATE.error, | ||
| 62 | info: { | ||
| 63 | message: err.message | ||
| 64 | } | ||
| 65 | }); | ||
| 66 | }); | ||
| 67 | } else { | ||
| 68 | this.reportUpdateState({ | ||
| 69 | phase: UPDATE_MODAL_STATE.applicationUpdateAvailable, | ||
| 70 | info: { | ||
| 71 | version: info.version, | ||
| 72 | message: parseReleaseMessage(info.releaseNotes, {html: true}) | ||
| 73 | } | ||
| 74 | }); | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | resourceAvailable (info) { | ||
| 79 | this.updateTarget = UPDATE_TARGET.resource; | ||
| 80 | this.reportUpdateState({ | ||
| 81 | phase: UPDATE_MODAL_STATE.resourceUpdateAvailable, | ||
| 82 | info: { | ||
| 83 | version: info.latestVersion, | ||
| 84 | message: info.message | ||
| 85 | } | ||
| 86 | }); | ||
| 87 | } | ||
| 88 | |||
| 89 | checkUpdateAtStartup () { | ||
| 90 | autoUpdater.on('error', err => { | ||
| 91 | this.removeAllAutoUpdaterListeners(); | ||
| 92 | console.warn(`Error while checking for application update: ${err}`); | ||
| 93 | }); | ||
| 94 | autoUpdater.once('update-available', applicationUpdateInfo => { | ||
| 95 | this.removeAllAutoUpdaterListeners(); | ||
| 96 | this.applicationAvailable(applicationUpdateInfo); | ||
| 97 | }); | ||
| 98 | autoUpdater.once('update-not-available', () => { | ||
| 99 | this.removeAllAutoUpdaterListeners(); | ||
| 100 | this._resourceServer.checkUpdate() | ||
| 101 | .then(resourceUpdateInfo => { | ||
| 102 | if (resourceUpdateInfo.updateble) { | ||
| 103 | this.resourceAvailable(resourceUpdateInfo); | ||
| 104 | } | ||
| 105 | }) | ||
| 106 | .catch(err => { | ||
| 107 | console.warn(`Error while checking for resource update: ${err}`); | ||
| 108 | }); | ||
| 109 | }); | ||
| 110 | |||
| 111 | autoUpdater.checkForUpdates(); | ||
| 112 | } | ||
| 113 | |||
| 114 | reqeustCheckUpdate () { | ||
| 115 | autoUpdater.on('error', err => { | ||
| 116 | this.removeAllAutoUpdaterListeners(); | ||
| 117 | if (err.message === 'net::ERR_INTERNET_DISCONNECTED') { | ||
| 118 | this.reportUpdateState({ | ||
| 119 | phase: UPDATE_MODAL_STATE.error, | ||
| 120 | info: { | ||
| 121 | message: formatMessage({ | ||
| 122 | id: 'index.internetDisconnectedError', | ||
| 123 | default: 'Internet disconnected, please verify your internet connection and try again.', | ||
| 124 | description: 'Error message of internet disconnected' | ||
| 125 | }) | ||
| 126 | } | ||
| 127 | }); | ||
| 128 | } else { | ||
| 129 | this.reportUpdateState({ | ||
| 130 | phase: UPDATE_MODAL_STATE.error, | ||
| 131 | info: { | ||
| 132 | message: err.message | ||
| 133 | } | ||
| 134 | }); | ||
| 135 | } | ||
| 136 | }); | ||
| 137 | autoUpdater.once('update-available', applicationUpdateInfo => { | ||
| 138 | this.updaterState = UPDATE_MODAL_STATE.applicationUpdateAvailable; | ||
| 139 | this.removeAllAutoUpdaterListeners(); | ||
| 140 | this.applicationAvailable(applicationUpdateInfo); | ||
| 141 | }); | ||
| 142 | autoUpdater.once('update-not-available', () => { | ||
| 143 | this.removeAllAutoUpdaterListeners(); | ||
| 144 | |||
| 145 | this.abortController = new AbortController(); | ||
| 146 | this._resourceServer.checkUpdate({signal: this.abortController.signal}) | ||
| 147 | .then(resourceUpdateInfo => { | ||
| 148 | if (resourceUpdateInfo.updateble) { | ||
| 149 | this.updaterState = UPDATE_MODAL_STATE.resourceUpdateAvailable; | ||
| 150 | this.resourceAvailable(resourceUpdateInfo); | ||
| 151 | } else { | ||
| 152 | this.reportUpdateState({phase: 'latest'}); | ||
| 153 | } | ||
| 154 | }) | ||
| 155 | .catch(err => { | ||
| 156 | this.reportUpdateState({phase: 'error', message: err}); | ||
| 157 | }); | ||
| 158 | this.updaterState = UPDATE_MODAL_STATE.checkingResource; | ||
| 159 | }); | ||
| 160 | |||
| 161 | autoUpdater.checkForUpdates(); | ||
| 162 | this.updaterState = UPDATE_MODAL_STATE.checkingApplication; | ||
| 163 | } | ||
| 164 | |||
| 165 | reqeustUpdate () { | ||
| 166 | if (this.updateTarget === UPDATE_TARGET.application) { | ||
| 167 | this.cancellationToken = new CancellationToken(); | ||
| 168 | autoUpdater.downloadUpdate(this.cancellationToken); | ||
| 169 | this.updaterState = UPDATE_MODAL_STATE.applicationDownloading; | ||
| 170 | |||
| 171 | const PROGRESS_BASE_VALUE = 0; | ||
| 172 | const PROGRESS_DOWNLOADING_PROGRESS_VALUE = 0.1; | ||
| 173 | const PROGRESS_STEP_INTERVAL = 0.5; // 0.5s | ||
| 174 | const PROGRESS_STEP_TIMEOUT = 20; // 20s | ||
| 175 | const PROGRESS_STEP_VALUE = (PROGRESS_DOWNLOADING_PROGRESS_VALUE - PROGRESS_BASE_VALUE) / | ||
| 176 | (PROGRESS_STEP_TIMEOUT / PROGRESS_STEP_INTERVAL); | ||
| 177 | |||
| 178 | let downloadInProgress = false; | ||
| 179 | |||
| 180 | const stepProgressBar = progress => { | ||
| 181 | this.startDownloadTimeout = setTimeout(() => { | ||
| 182 | if (!downloadInProgress && progress <= PROGRESS_DOWNLOADING_PROGRESS_VALUE) { | ||
| 183 | this.reportUpdateState({ | ||
| 184 | phase: UPDATE_MODAL_STATE.applicationDownloading, | ||
| 185 | info: { | ||
| 186 | progress: progress | ||
| 187 | } | ||
| 188 | }); | ||
| 189 | stepProgressBar(progress + PROGRESS_STEP_VALUE); | ||
| 190 | } else { | ||
| 191 | this.startDownloadTimeout = null; | ||
| 192 | } | ||
| 193 | }, PROGRESS_STEP_INTERVAL * 1000); | ||
| 194 | }; | ||
| 195 | |||
| 196 | // After start downloading, it takes a while for download-progress event to trigger, | ||
| 197 | // report a progress that grows slowly over time let user know the downloading is started and running. | ||
| 198 | this.reportUpdateState({ | ||
| 199 | phase: UPDATE_MODAL_STATE.applicationDownloading, | ||
| 200 | info: { | ||
| 201 | progress: PROGRESS_BASE_VALUE | ||
| 202 | } | ||
| 203 | }); | ||
| 204 | stepProgressBar(PROGRESS_BASE_VALUE); | ||
| 205 | |||
| 206 | return new Promise((resolve, reject) => { | ||
| 207 | |||
| 208 | autoUpdater.on('error', err => reject(err)); | ||
| 209 | |||
| 210 | autoUpdater.on('download-progress', progressObj => { | ||
| 211 | downloadInProgress = true; | ||
| 212 | this.reportUpdateState({ | ||
| 213 | phase: UPDATE_MODAL_STATE.applicationDownloading, | ||
| 214 | info: { | ||
| 215 | progress: ((progressObj.percent * (1 - PROGRESS_DOWNLOADING_PROGRESS_VALUE)) + | ||
| 216 | (PROGRESS_DOWNLOADING_PROGRESS_VALUE * 100)) / 100, | ||
| 217 | state: { | ||
| 218 | speed: `${bytes(progressObj.bytesPerSecond)}/s`, | ||
| 219 | total: bytes(progressObj.total), | ||
| 220 | done: bytes(progressObj.transferred) | ||
| 221 | } | ||
| 222 | } | ||
| 223 | }); | ||
| 224 | }); | ||
| 225 | |||
| 226 | autoUpdater.on('update-downloaded', () => { | ||
| 227 | this.reportUpdateState({phase: UPDATE_MODAL_STATE.applicationDownloadFinish}); | ||
| 228 | setTimeout(() => { | ||
| 229 | console.log(`INFO: App will quit and install after 3 seconds`); | ||
| 230 | autoUpdater.quitAndInstall(); | ||
| 231 | }, 1000 * 3); | ||
| 232 | }); | ||
| 233 | }); | ||
| 234 | |||
| 235 | } | ||
| 236 | const reportResourceUpdateState = res => { | ||
| 237 | if (this.updaterState !== UPDATE_MODAL_STATE.abort) { | ||
| 238 | this.reportUpdateState({ | ||
| 239 | phase: UPDATE_MODAL_STATE.resourceUpdating, | ||
| 240 | info: { | ||
| 241 | phase: res.phase, | ||
| 242 | progress: res.progress, | ||
| 243 | state: res.state | ||
| 244 | } | ||
| 245 | }); | ||
| 246 | } | ||
| 247 | }; | ||
| 248 | |||
| 249 | this.abortController = new AbortController(); | ||
| 250 | |||
| 251 | this.updaterState = UPDATE_MODAL_STATE.resourceUpdating; | ||
| 252 | return this._resourceServer.update({ | ||
| 253 | signal: this.abortController.signal, | ||
| 254 | callback: reportResourceUpdateState | ||
| 255 | }) | ||
| 256 | .then(() => { | ||
| 257 | this.reportUpdateState({phase: UPDATE_MODAL_STATE.resourceUpdatFinish}); | ||
| 258 | return Promise.resolve(); | ||
| 259 | }) | ||
| 260 | .catch(err => { | ||
| 261 | if (!err.stack.startsWith('AbortError')) { | ||
| 262 | this.reportUpdateState({ | ||
| 263 | phase: UPDATE_MODAL_STATE.error, | ||
| 264 | info: { | ||
| 265 | message: err.message | ||
| 266 | } | ||
| 267 | }); | ||
| 268 | } | ||
| 269 | return Promise.reject(err); | ||
| 270 | }); | ||
| 271 | |||
| 272 | } | ||
| 273 | |||
| 274 | abortUpdate () { | ||
| 275 | if (this.updaterState === UPDATE_MODAL_STATE.checkingResource || | ||
| 276 | this.updaterState === UPDATE_MODAL_STATE.resourceUpdating) { | ||
| 277 | this.updaterState = UPDATE_MODAL_STATE.abort; | ||
| 278 | this.abortController.abort(); | ||
| 279 | } else if (this.updaterState === UPDATE_MODAL_STATE.checkingApplication) { | ||
| 280 | this.removeAllAutoUpdaterListeners(); | ||
| 281 | } else if (this.updaterState === UPDATE_MODAL_STATE.applicationDownloading) { | ||
| 282 | this.removeAllAutoUpdaterListeners(); | ||
| 283 | this.cancellationToken.cancel(); | ||
| 284 | if (this.startDownloadTimeout) { | ||
| 285 | clearTimeout(this.startDownloadTimeout); | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | if (this.updaterState !== UPDATE_MODAL_STATE.abort) { | ||
| 290 | this.updaterState = null; | ||
| 291 | } | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | export default OpenblockDesktopUpdater; |
src/main/argv.js
0 → 100644
| 1 | import minimist from 'minimist'; | ||
| 2 | |||
| 3 | // inspired by yargs' process-argv | ||
| 4 | export const isElectronApp = () => !!process.versions.electron; | ||
| 5 | export const isElectronBundledApp = () => isElectronApp() && !process.defaultApp; | ||
| 6 | |||
| 7 | export const parseAndTrimArgs = argv => { | ||
| 8 | // bundled Electron app: ignore 1 from "my-app arg1 arg2" | ||
| 9 | // unbundled Electron app: ignore 2 from "electron main/index.js arg1 arg2" | ||
| 10 | // node.js app: ignore 2 from "node src/index.js arg1 arg2" | ||
| 11 | const ignoreCount = isElectronBundledApp() ? 1 : 2; | ||
| 12 | |||
| 13 | const parsed = minimist(argv); | ||
| 14 | |||
| 15 | // ignore arguments AFTER parsing to handle cases like "electron --inspect=42 my.js arg1 arg2" | ||
| 16 | parsed._ = parsed._.slice(ignoreCount); | ||
| 17 | |||
| 18 | return parsed; | ||
| 19 | }; | ||
| 20 | |||
| 21 | const argv = parseAndTrimArgs(process.argv); | ||
| 22 | |||
| 23 | export default argv; |
src/main/index.js
0 → 100644
| 1 | import {BrowserWindow, Menu, app, dialog, ipcMain, shell, systemPreferences} from 'electron'; | ||
| 2 | import * as remote from '@electron/remote/main'; | ||
| 3 | import fs from 'fs-extra'; | ||
| 4 | import path from 'path'; | ||
| 5 | import {URL} from 'url'; | ||
| 6 | import {promisify} from 'util'; | ||
| 7 | |||
| 8 | import argv from './argv'; | ||
| 9 | import {getFilterForExtension} from './FileFilters'; | ||
| 10 | import telemetry from './OpenblockDesktopTelemetry'; | ||
| 11 | import Updater from './OpenblockDesktopUpdater'; | ||
| 12 | import DesktopLink from './OpenblockDesktopLink.js'; | ||
| 13 | import MacOSMenu from './MacOSMenu'; | ||
| 14 | import log from '../common/log.js'; | ||
| 15 | import {productName, version} from '../../package.json'; | ||
| 16 | |||
| 17 | import {v4 as uuidv4} from 'uuid'; | ||
| 18 | import ElectronStore from 'electron-store'; | ||
| 19 | import formatMessage from 'format-message'; | ||
| 20 | import locales from 'openblock-l10n/locales/desktop-msgs'; | ||
| 21 | |||
| 22 | const storage = new ElectronStore(); | ||
| 23 | const desktopLink = new DesktopLink(); | ||
| 24 | |||
| 25 | formatMessage.setup({translations: locales}); | ||
| 26 | |||
| 27 | // suppress deprecation warning; this will be the default in Electron 9 | ||
| 28 | app.allowRendererProcessReuse = true; | ||
| 29 | |||
| 30 | // allow connect to localhost | ||
| 31 | app.commandLine.appendSwitch('allow-insecure-localhost', 'true'); | ||
| 32 | |||
| 33 | // enable gpu and ignore gpu blacklist | ||
| 34 | app.commandLine.hasSwitch('enable-gpu'); | ||
| 35 | app.commandLine.hasSwitch('ignore-gpu-blacklist'); | ||
| 36 | |||
| 37 | telemetry.appWasOpened(); | ||
| 38 | |||
| 39 | const defaultSize = {width: 1620, height: 900}; | ||
| 40 | |||
| 41 | const isDevelopment = process.env.NODE_ENV !== 'production'; | ||
| 42 | |||
| 43 | const devToolKey = ((process.platform === 'darwin') ? | ||
| 44 | { // macOS: command+option+i | ||
| 45 | alt: true, // option | ||
| 46 | control: false, | ||
| 47 | meta: true, // command | ||
| 48 | shift: false, | ||
| 49 | code: 'KeyI' | ||
| 50 | } : { // Windows: control+shift+i | ||
| 51 | alt: false, | ||
| 52 | control: true, | ||
| 53 | meta: false, // Windows key | ||
| 54 | shift: true, | ||
| 55 | code: 'KeyI' | ||
| 56 | } | ||
| 57 | ); | ||
| 58 | |||
| 59 | // global window references prevent them from being garbage-collected | ||
| 60 | const _windows = {}; | ||
| 61 | |||
| 62 | // enable connecting to Scratch Link even if we DNS / Internet access is not available | ||
| 63 | // this must happen BEFORE the app ready event! | ||
| 64 | app.commandLine.appendSwitch('host-resolver-rules', 'MAP device-manager.scratch.mit.edu 127.0.0.1'); | ||
| 65 | |||
| 66 | const displayPermissionDeniedWarning = (browserWindow, permissionType) => { | ||
| 67 | let title; | ||
| 68 | let message; | ||
| 69 | switch (permissionType) { | ||
| 70 | case 'camera': | ||
| 71 | title = formatMessage({ | ||
| 72 | id: 'index.cameraPermissionDeniedTitle', | ||
| 73 | default: 'Camera Permission Denied', | ||
| 74 | description: 'prompt for camera permission denied' | ||
| 75 | }); | ||
| 76 | message = formatMessage({ | ||
| 77 | id: 'index.cameraPermissionDeniedMessage', | ||
| 78 | default: 'Permission to use the camera has been denied. ' + | ||
| 79 | 'OpenBlock will not be able to take a photo or use video sensing blocks.', | ||
| 80 | description: 'message for camera permission denied' | ||
| 81 | }); | ||
| 82 | break; | ||
| 83 | case 'microphone': | ||
| 84 | title = formatMessage({ | ||
| 85 | id: 'index.microphonePermissionDeniedTitle', | ||
| 86 | default: 'Microphone Permission Denied', | ||
| 87 | description: 'prompt for microphone permission denied' | ||
| 88 | }); | ||
| 89 | message = formatMessage({ | ||
| 90 | id: 'index.microphonePermissionDeniedMessage', | ||
| 91 | default: 'Permission to use the microphone has been denied. ' + | ||
| 92 | 'OpenBlock will not be able to record sounds or detect loudness.', | ||
| 93 | description: 'message for microphone permission denied' | ||
| 94 | }); | ||
| 95 | break; | ||
| 96 | default: // shouldn't ever happen... | ||
| 97 | title = formatMessage({ | ||
| 98 | id: 'index.permissionDeniedTitle', | ||
| 99 | default: 'Permission Denied', | ||
| 100 | description: 'prompt for permission denied' | ||
| 101 | }); | ||
| 102 | message = formatMessage({ | ||
| 103 | id: 'index.permissionDeniedMessage', | ||
| 104 | default: 'A permission has been denied.', | ||
| 105 | description: 'message for permission denied' | ||
| 106 | }); | ||
| 107 | } | ||
| 108 | |||
| 109 | let instructions; | ||
| 110 | switch (process.platform) { | ||
| 111 | case 'darwin': | ||
| 112 | instructions = formatMessage({ | ||
| 113 | id: 'index.darwinPermissionDeniedInstructions', | ||
| 114 | default: 'To change OpenBlock permissions, please check "Security & Privacy" in System Preferences.', | ||
| 115 | description: 'prompt for fix darwin permission denied instructions' | ||
| 116 | }); | ||
| 117 | break; | ||
| 118 | default: | ||
| 119 | instructions = formatMessage({ | ||
| 120 | id: 'index.permissionDeniedInstructions', | ||
| 121 | default: 'To change OpenBlock permissions, please check your system settings and restart OpenBlock.', | ||
| 122 | description: 'prompt for fix permission denied instructions' | ||
| 123 | }); | ||
| 124 | break; | ||
| 125 | } | ||
| 126 | message = `${message}\n\n${instructions}`; | ||
| 127 | |||
| 128 | dialog.showMessageBox(browserWindow, {type: 'warning', title, message}); | ||
| 129 | }; | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Build an absolute URL from a relative one, optionally adding search query parameters. | ||
| 133 | * The base of the URL will depend on whether or not the application is running in development mode. | ||
| 134 | * @param {string} url - the relative URL, like 'index.html' | ||
| 135 | * @param {*} search - the optional "search" parameters (the part of the URL after '?'), like "route=about" | ||
| 136 | * @returns {string} - an absolute URL as a string | ||
| 137 | */ | ||
| 138 | const makeFullUrl = (url, search = null) => { | ||
| 139 | const baseUrl = (isDevelopment ? | ||
| 140 | `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}/` : | ||
| 141 | `file://${__dirname}/` | ||
| 142 | ); | ||
| 143 | const fullUrl = new URL(url, baseUrl); | ||
| 144 | if (search) { | ||
| 145 | fullUrl.search = search; // automatically percent-encodes anything that needs it | ||
| 146 | } | ||
| 147 | return fullUrl.toString(); | ||
| 148 | }; | ||
| 149 | |||
| 150 | /** | ||
| 151 | * Prompt in a platform-specific way for permission to access the microphone or camera, if Electron supports doing so. | ||
| 152 | * Any application-level checks, such as whether or not a particular frame or document should be allowed to ask, | ||
| 153 | * should be done before calling this function. | ||
| 154 | * This function may return a Promise! | ||
| 155 | * | ||
| 156 | * @param {string} mediaType - one of Electron's media types, like 'microphone' or 'camera' | ||
| 157 | * @returns {boolean|Promise.<boolean>} - true if permission granted, false otherwise. | ||
| 158 | */ | ||
| 159 | const askForMediaAccess = mediaType => { | ||
| 160 | if (systemPreferences.askForMediaAccess) { | ||
| 161 | // Electron currently only implements this on macOS | ||
| 162 | // This returns a Promise | ||
| 163 | return systemPreferences.askForMediaAccess(mediaType); | ||
| 164 | } | ||
| 165 | // For other platforms we can't reasonably do anything other than assume we have access. | ||
| 166 | return true; | ||
| 167 | }; | ||
| 168 | |||
| 169 | const handlePermissionRequest = async (webContents, permission, callback, details) => { | ||
| 170 | if (webContents !== _windows.main.webContents) { | ||
| 171 | // deny: request came from somewhere other than the main window's web contents | ||
| 172 | return callback(false); | ||
| 173 | } | ||
| 174 | if (!details.isMainFrame) { | ||
| 175 | // deny: request came from a subframe of the main window, not the main frame | ||
| 176 | return callback(false); | ||
| 177 | } | ||
| 178 | if (permission !== 'media') { | ||
| 179 | // deny: request is for some other kind of access like notifications or pointerLock | ||
| 180 | return callback(false); | ||
| 181 | } | ||
| 182 | const requiredBase = makeFullUrl(''); | ||
| 183 | if (details.requestingUrl.indexOf(requiredBase) !== 0) { | ||
| 184 | // deny: request came from a URL outside of our "sandbox" | ||
| 185 | return callback(false); | ||
| 186 | } | ||
| 187 | let askForMicrophone = false; | ||
| 188 | let askForCamera = false; | ||
| 189 | for (const mediaType of details.mediaTypes) { | ||
| 190 | switch (mediaType) { | ||
| 191 | case 'audio': | ||
| 192 | askForMicrophone = true; | ||
| 193 | break; | ||
| 194 | case 'video': | ||
| 195 | askForCamera = true; | ||
| 196 | break; | ||
| 197 | default: | ||
| 198 | // deny: unhandled media type | ||
| 199 | return callback(false); | ||
| 200 | } | ||
| 201 | } | ||
| 202 | const parentWindow = _windows.main; // if we ever allow media in non-main windows we'll also need to change this | ||
| 203 | if (askForMicrophone) { | ||
| 204 | const microphoneResult = await askForMediaAccess('microphone'); | ||
| 205 | if (!microphoneResult) { | ||
| 206 | displayPermissionDeniedWarning(parentWindow, 'microphone'); | ||
| 207 | return callback(false); | ||
| 208 | } | ||
| 209 | } | ||
| 210 | if (askForCamera) { | ||
| 211 | const cameraResult = await askForMediaAccess('camera'); | ||
| 212 | if (!cameraResult) { | ||
| 213 | displayPermissionDeniedWarning(parentWindow, 'camera'); | ||
| 214 | return callback(false); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | return callback(true); | ||
| 218 | }; | ||
| 219 | |||
| 220 | const createWindow = ({search = null, url = 'index.html', ...browserWindowOptions}) => { | ||
| 221 | const window = new BrowserWindow({ | ||
| 222 | useContentSize: true, | ||
| 223 | show: false, | ||
| 224 | webPreferences: { | ||
| 225 | contextIsolation: false, | ||
| 226 | nodeIntegration: true | ||
| 227 | }, | ||
| 228 | ...browserWindowOptions | ||
| 229 | }); | ||
| 230 | const webContents = window.webContents; | ||
| 231 | |||
| 232 | webContents.session.setPermissionRequestHandler(handlePermissionRequest); | ||
| 233 | |||
| 234 | webContents.on('before-input-event', (event, input) => { | ||
| 235 | if (input.code === devToolKey.code && | ||
| 236 | input.alt === devToolKey.alt && | ||
| 237 | input.control === devToolKey.control && | ||
| 238 | input.meta === devToolKey.meta && | ||
| 239 | input.shift === devToolKey.shift && | ||
| 240 | input.type === 'keyDown' && | ||
| 241 | !input.isAutoRepeat && | ||
| 242 | !input.isComposing) { | ||
| 243 | event.preventDefault(); | ||
| 244 | webContents.openDevTools({mode: 'detach', activate: true}); | ||
| 245 | } | ||
| 246 | }); | ||
| 247 | |||
| 248 | webContents.on('new-window', (event, newWindowUrl) => { | ||
| 249 | shell.openExternal(newWindowUrl); | ||
| 250 | event.preventDefault(); | ||
| 251 | }); | ||
| 252 | |||
| 253 | const fullUrl = makeFullUrl(url, search); | ||
| 254 | window.loadURL(fullUrl); | ||
| 255 | window.once('ready-to-show', () => { | ||
| 256 | webContents.send('ready-to-show'); | ||
| 257 | }); | ||
| 258 | |||
| 259 | return window; | ||
| 260 | }; | ||
| 261 | |||
| 262 | const createAboutWindow = () => { | ||
| 263 | const window = createWindow({ | ||
| 264 | width: 400, | ||
| 265 | height: 400, | ||
| 266 | parent: _windows.main, | ||
| 267 | search: 'route=about', | ||
| 268 | title: `About ${productName}` | ||
| 269 | }); | ||
| 270 | return window; | ||
| 271 | }; | ||
| 272 | |||
| 273 | const createLicenseWindow = () => { | ||
| 274 | const window = createWindow({ | ||
| 275 | width: _windows.main.width * 0.8, | ||
| 276 | height: _windows.main.height * 0.8, | ||
| 277 | parent: _windows.main, | ||
| 278 | search: 'route=license', | ||
| 279 | title: `${productName} License` | ||
| 280 | }); | ||
| 281 | return window; | ||
| 282 | }; | ||
| 283 | |||
| 284 | const createPrivacyWindow = () => { | ||
| 285 | const window = createWindow({ | ||
| 286 | width: _windows.main.width * 0.8, | ||
| 287 | height: _windows.main.height * 0.8, | ||
| 288 | parent: _windows.main, | ||
| 289 | search: 'route=privacy', | ||
| 290 | title: `${productName} Privacy Policy` | ||
| 291 | }); | ||
| 292 | return window; | ||
| 293 | }; | ||
| 294 | |||
| 295 | const createLoadingWindow = () => { | ||
| 296 | const window = createWindow({ | ||
| 297 | width: 800, | ||
| 298 | height: 150, | ||
| 299 | frame: false, | ||
| 300 | resizable: false, | ||
| 301 | transparent: true, | ||
| 302 | hasShadow: false, | ||
| 303 | search: 'route=loading', | ||
| 304 | title: `Loding ${productName} ${version}` | ||
| 305 | }); | ||
| 306 | |||
| 307 | window.once('ready-to-show', () => { | ||
| 308 | window.show(); | ||
| 309 | }); | ||
| 310 | |||
| 311 | return window; | ||
| 312 | }; | ||
| 313 | |||
| 314 | const getIsProjectSave = downloadItem => { | ||
| 315 | switch (downloadItem.getMimeType()) { | ||
| 316 | case 'application/x.openblock.ob': | ||
| 317 | return true; | ||
| 318 | } | ||
| 319 | return false; | ||
| 320 | }; | ||
| 321 | |||
| 322 | const createMainWindow = () => { | ||
| 323 | const window = createWindow({ | ||
| 324 | width: defaultSize.width, | ||
| 325 | height: defaultSize.height, | ||
| 326 | title: `${productName} ${version}` // something like "Scratch 3.14" | ||
| 327 | }); | ||
| 328 | const webContents = window.webContents; | ||
| 329 | |||
| 330 | const update = new Updater(webContents, desktopLink.resourceServer); | ||
| 331 | remote.initialize(); | ||
| 332 | remote.enable(webContents); | ||
| 333 | |||
| 334 | webContents.session.on('will-download', (willDownloadEvent, downloadItem) => { | ||
| 335 | const isProjectSave = getIsProjectSave(downloadItem); | ||
| 336 | const itemPath = downloadItem.getFilename(); | ||
| 337 | const baseName = path.basename(itemPath); | ||
| 338 | const extName = path.extname(baseName); | ||
| 339 | const options = { | ||
| 340 | defaultPath: baseName | ||
| 341 | }; | ||
| 342 | if (extName) { | ||
| 343 | const extNameNoDot = extName.replace(/^\./, ''); | ||
| 344 | options.filters = [getFilterForExtension(extNameNoDot)]; | ||
| 345 | } | ||
| 346 | const userChosenPath = dialog.showSaveDialogSync(window, options); | ||
| 347 | // this will be falsy if the user canceled the save | ||
| 348 | if (userChosenPath) { | ||
| 349 | const userBaseName = path.basename(userChosenPath); | ||
| 350 | const tempPath = path.join(app.getPath('temp'), userBaseName); | ||
| 351 | |||
| 352 | // WARNING: `setSavePath` on this item is only valid during the `will-download` event. Calling the async | ||
| 353 | // version of `showSaveDialog` means the event will finish before we get here, so `setSavePath` will be | ||
| 354 | // ignored. For that reason we need to call `showSaveDialogSync` above. | ||
| 355 | downloadItem.setSavePath(tempPath); | ||
| 356 | |||
| 357 | downloadItem.on('done', async (doneEvent, doneState) => { | ||
| 358 | try { | ||
| 359 | if (doneState !== 'completed') { | ||
| 360 | // The download was canceled or interrupted. Cancel the telemetry event and delete the file. | ||
| 361 | throw new Error(`save ${doneState}`); // "save cancelled" or "save interrupted" | ||
| 362 | } | ||
| 363 | await fs.move(tempPath, userChosenPath, {overwrite: true}); | ||
| 364 | if (isProjectSave) { | ||
| 365 | const newProjectTitle = path.basename(userChosenPath, extName); | ||
| 366 | webContents.send('setTitleFromSave', {title: newProjectTitle}); | ||
| 367 | |||
| 368 | // "setTitleFromSave" will set the project title but GUI has already reported the telemetry | ||
| 369 | // event using the old title. This call lets the telemetry client know that the save was | ||
| 370 | // actually completed and the event should be committed to the event queue with this new title. | ||
| 371 | telemetry.projectSaveCompleted(newProjectTitle); | ||
| 372 | } | ||
| 373 | } catch (e) { | ||
| 374 | if (isProjectSave) { | ||
| 375 | telemetry.projectSaveCanceled(); | ||
| 376 | } | ||
| 377 | // don't clean up until after the message box to allow troubleshooting / recovery | ||
| 378 | await dialog.showMessageBox(window, { | ||
| 379 | type: 'error', | ||
| 380 | title: formatMessage({ | ||
| 381 | id: 'index.saveFailedTitle', | ||
| 382 | default: 'Failed to save project', | ||
| 383 | description: 'Title for save failed' | ||
| 384 | }), | ||
| 385 | message: `${formatMessage({ | ||
| 386 | id: 'index.saveFailed', | ||
| 387 | default: 'Save failed:', | ||
| 388 | description: 'prompt for save failed' | ||
| 389 | })}\n${userChosenPath}`, | ||
| 390 | detail: e.message | ||
| 391 | }); | ||
| 392 | fs.exists(tempPath).then(exists => { | ||
| 393 | if (exists) { | ||
| 394 | fs.unlink(tempPath); | ||
| 395 | } | ||
| 396 | }); | ||
| 397 | } | ||
| 398 | }); | ||
| 399 | } else { | ||
| 400 | downloadItem.cancel(); | ||
| 401 | if (isProjectSave) { | ||
| 402 | telemetry.projectSaveCanceled(); | ||
| 403 | } | ||
| 404 | } | ||
| 405 | }); | ||
| 406 | |||
| 407 | webContents.on('will-prevent-unload', ev => { | ||
| 408 | const choice = dialog.showMessageBoxSync(window, { | ||
| 409 | title: productName, | ||
| 410 | type: 'question', | ||
| 411 | message: formatMessage({ | ||
| 412 | id: 'index.questionLeave', | ||
| 413 | default: 'Leave Openblock?', | ||
| 414 | description: 'prompt for leave Openblock' | ||
| 415 | }), | ||
| 416 | detail: formatMessage({ | ||
| 417 | id: 'index.questionLeaveDetail', | ||
| 418 | default: 'Any unsaved changes will be lost.', | ||
| 419 | description: 'detail prompt for leave Openblock' | ||
| 420 | }), | ||
| 421 | buttons: [ | ||
| 422 | formatMessage({ | ||
| 423 | id: 'index.stay', | ||
| 424 | default: 'Stay', | ||
| 425 | description: 'Label for stay' | ||
| 426 | }), formatMessage({ | ||
| 427 | id: 'index.leave', | ||
| 428 | default: 'Leave', | ||
| 429 | description: 'Label for leave' | ||
| 430 | }) | ||
| 431 | ], | ||
| 432 | cancelId: 0, // closing the dialog means "stay" | ||
| 433 | defaultId: 0 // pressing enter or space without explicitly selecting something means "stay" | ||
| 434 | }); | ||
| 435 | const shouldQuit = (choice === 1); | ||
| 436 | if (shouldQuit) { | ||
| 437 | ev.preventDefault(); | ||
| 438 | } | ||
| 439 | }); | ||
| 440 | |||
| 441 | ipcMain.on('loading-completed', () => { | ||
| 442 | if (!storage.has('userId')) { | ||
| 443 | storage.set('userId', uuidv4()); | ||
| 444 | } | ||
| 445 | const userId = storage.get('userId'); | ||
| 446 | webContents.send('setUserId', userId); | ||
| 447 | |||
| 448 | webContents.send('setPlatform', process.platform); | ||
| 449 | |||
| 450 | update.checkUpdateAtStartup(); | ||
| 451 | }); | ||
| 452 | |||
| 453 | ipcMain.on('reqeustCheckUpdate', () => { | ||
| 454 | update.reqeustCheckUpdate(); | ||
| 455 | }); | ||
| 456 | |||
| 457 | ipcMain.on('reqeustUpdate', () => { | ||
| 458 | update.reqeustUpdate() | ||
| 459 | .then(() => { | ||
| 460 | setTimeout(() => { | ||
| 461 | console.log(`INFO: App will restart after 3 seconds`); | ||
| 462 | app.relaunch(); | ||
| 463 | app.exit(); | ||
| 464 | }, 1000 * 3); | ||
| 465 | }) | ||
| 466 | .catch(err => { | ||
| 467 | console.error(`ERR!: update failed: ${err}`); | ||
| 468 | }); | ||
| 469 | }); | ||
| 470 | |||
| 471 | ipcMain.on('abortUpdate', () => { | ||
| 472 | update.abortUpdate(); | ||
| 473 | }); | ||
| 474 | |||
| 475 | return window; | ||
| 476 | }; | ||
| 477 | |||
| 478 | if (process.platform === 'darwin') { | ||
| 479 | const osxMenu = Menu.buildFromTemplate(MacOSMenu(app)); | ||
| 480 | Menu.setApplicationMenu(osxMenu); | ||
| 481 | } else { | ||
| 482 | // disable menu for other platforms | ||
| 483 | Menu.setApplicationMenu(null); | ||
| 484 | } | ||
| 485 | |||
| 486 | // quit application when all windows are closed | ||
| 487 | app.on('window-all-closed', () => { | ||
| 488 | app.quit(); | ||
| 489 | }); | ||
| 490 | |||
| 491 | app.on('will-quit', () => { | ||
| 492 | telemetry.appWillClose(); | ||
| 493 | }); | ||
| 494 | |||
| 495 | app.on('activate', () => { | ||
| 496 | if (_windows.main === null) { | ||
| 497 | createMainWindow(); | ||
| 498 | } | ||
| 499 | }); | ||
| 500 | |||
| 501 | // work around https://github.com/MarshallOfSound/electron-devtools-installer/issues/122 | ||
| 502 | // which seems to be a result of https://github.com/electron/electron/issues/19468 | ||
| 503 | if (process.platform === 'win32') { | ||
| 504 | const appUserDataPath = app.getPath('userData'); | ||
| 505 | const devToolsExtensionsPath = path.join(appUserDataPath, 'DevTools Extensions'); | ||
| 506 | try { | ||
| 507 | fs.unlinkSync(devToolsExtensionsPath); | ||
| 508 | } catch (_) { | ||
| 509 | // don't complain if the file doesn't exist | ||
| 510 | } | ||
| 511 | } | ||
| 512 | |||
| 513 | // create main BrowserWindow when electron is ready | ||
| 514 | app.on('ready', () => { | ||
| 515 | if (isDevelopment) { | ||
| 516 | import('electron-devtools-installer').then(importedModule => { | ||
| 517 | const {default: installExtension, ...devToolsExtensions} = importedModule; | ||
| 518 | const extensionsToInstall = [ | ||
| 519 | devToolsExtensions.REACT_DEVELOPER_TOOLS, | ||
| 520 | devToolsExtensions.REACT_PERF, | ||
| 521 | devToolsExtensions.REDUX_DEVTOOLS | ||
| 522 | ]; | ||
| 523 | for (const extension of extensionsToInstall) { | ||
| 524 | // WARNING: depending on a lot of things including the version of Electron `installExtension` might | ||
| 525 | // return a promise that never resolves, especially if the extension is already installed. | ||
| 526 | installExtension(extension).then( | ||
| 527 | extensionName => log(`Installed dev extension: ${extensionName}`), | ||
| 528 | errorMessage => log.error(`Error installing dev extension: ${errorMessage}`) | ||
| 529 | ); | ||
| 530 | } | ||
| 531 | }); | ||
| 532 | } | ||
| 533 | |||
| 534 | ipcMain.on('clearCache', () => { | ||
| 535 | desktopLink.clearCache(); | ||
| 536 | }); | ||
| 537 | |||
| 538 | ipcMain.on('installDriver', () => { | ||
| 539 | desktopLink.installDriver(() => { | ||
| 540 | dialog.showMessageBox(_windows.main, { | ||
| 541 | type: 'info', | ||
| 542 | message: `${formatMessage({ | ||
| 543 | id: 'index.systemRestartRequired', | ||
| 544 | default: 'Installation is complete, please restart the system.', | ||
| 545 | description: 'prompt for restart system' | ||
| 546 | })}` | ||
| 547 | }); | ||
| 548 | }); | ||
| 549 | }); | ||
| 550 | |||
| 551 | // create a loading windows let user know the app is starting | ||
| 552 | _windows.loading = createLoadingWindow(); | ||
| 553 | _windows.loading.once('show', () => { | ||
| 554 | // TODO: This code should be deleted afterwards | ||
| 555 | // Due to the changes in cache logic updates, before adding the online | ||
| 556 | // library version, clear the cache directly to prevent the old cache | ||
| 557 | // content from interfering with the operation of the new version. | ||
| 558 | desktopLink.clearCache(false); | ||
| 559 | |||
| 560 | desktopLink.start(); | ||
| 561 | |||
| 562 | _windows.main = createMainWindow(); | ||
| 563 | _windows.main.on('closed', () => { | ||
| 564 | delete _windows.main; | ||
| 565 | }); | ||
| 566 | |||
| 567 | // In order to fix the bug caused by using alert on windows | ||
| 568 | // https://github.com/electron/electron/issues/20400 | ||
| 569 | if (process.platform === 'win32') { | ||
| 570 | let needsFocusFix = false; | ||
| 571 | let triggeringProgrammaticBlur = false; | ||
| 572 | _windows.main.on('blur', () => { | ||
| 573 | if (!triggeringProgrammaticBlur) { | ||
| 574 | needsFocusFix = true; | ||
| 575 | } | ||
| 576 | }); | ||
| 577 | _windows.main.on('focus', () => { | ||
| 578 | if (needsFocusFix) { | ||
| 579 | needsFocusFix = false; | ||
| 580 | triggeringProgrammaticBlur = true; | ||
| 581 | setTimeout(() => { | ||
| 582 | if (_windows.main) { | ||
| 583 | _windows.main.blur(); | ||
| 584 | _windows.main.focus(); | ||
| 585 | setTimeout(() => { | ||
| 586 | triggeringProgrammaticBlur = false; | ||
| 587 | }, 100); | ||
| 588 | } | ||
| 589 | }, 100); | ||
| 590 | } | ||
| 591 | }); | ||
| 592 | } | ||
| 593 | |||
| 594 | _windows.about = createAboutWindow(); | ||
| 595 | _windows.about.on('close', event => { | ||
| 596 | event.preventDefault(); | ||
| 597 | _windows.about.hide(); | ||
| 598 | }); | ||
| 599 | _windows.license = createLicenseWindow(); | ||
| 600 | _windows.license.on('close', event => { | ||
| 601 | event.preventDefault(); | ||
| 602 | _windows.license.hide(); | ||
| 603 | }); | ||
| 604 | _windows.privacy = createPrivacyWindow(); | ||
| 605 | _windows.privacy.on('close', event => { | ||
| 606 | event.preventDefault(); | ||
| 607 | _windows.privacy.hide(); | ||
| 608 | }); | ||
| 609 | |||
| 610 | // after finsh load progress show main window and close loading window | ||
| 611 | _windows.main.show(); | ||
| 612 | _windows.loading.close(); | ||
| 613 | delete _windows.loading; | ||
| 614 | }); | ||
| 615 | }); | ||
| 616 | |||
| 617 | ipcMain.on('open-about-window', () => { | ||
| 618 | _windows.about.show(); | ||
| 619 | }); | ||
| 620 | |||
| 621 | ipcMain.on('open-license-window', () => { | ||
| 622 | _windows.license.show(); | ||
| 623 | }); | ||
| 624 | |||
| 625 | ipcMain.on('open-privacy-policy-window', () => { | ||
| 626 | _windows.privacy.show(); | ||
| 627 | }); | ||
| 628 | |||
| 629 | ipcMain.on('set-locale', (event, arg) => { | ||
| 630 | formatMessage.setup({locale: arg}); | ||
| 631 | }); | ||
| 632 | |||
| 633 | |||
| 634 | // start loading initial project data before the GUI needs it so the load seems faster | ||
| 635 | const initialProjectDataPromise = (async () => { | ||
| 636 | if (argv._.length === 0) { | ||
| 637 | // no command line argument means no initial project data | ||
| 638 | return; | ||
| 639 | } | ||
| 640 | if (argv._.length > 1) { | ||
| 641 | log.warn(`Expected 1 command line argument but received ${argv._.length}.`); | ||
| 642 | } | ||
| 643 | const projectPath = argv._[argv._.length - 1]; | ||
| 644 | try { | ||
| 645 | const projectData = await promisify(fs.readFile)(projectPath, null); | ||
| 646 | return projectData; | ||
| 647 | } catch (e) { | ||
| 648 | dialog.showMessageBox(_windows.main, { | ||
| 649 | type: 'error', | ||
| 650 | title: 'Failed to load project', | ||
| 651 | message: `${formatMessage({ | ||
| 652 | id: 'index.failedLoadProject', | ||
| 653 | default: 'Could not load project from file:', | ||
| 654 | description: 'prompt for failed to load project' | ||
| 655 | })}\n${projectPath}`, | ||
| 656 | detail: e.message | ||
| 657 | }); | ||
| 658 | } | ||
| 659 | // load failed: initial project data undefined | ||
| 660 | })(); // IIFE | ||
| 661 | |||
| 662 | ipcMain.handle('get-initial-project-data', () => initialProjectDataPromise); |
src/main/telemetry/TelemetryClient.js
0 → 100644
| 1 | import ElectronStore from 'electron-store'; | ||
| 2 | import nets from 'nets'; | ||
| 3 | import * as os from 'os'; | ||
| 4 | import { | ||
| 5 | v1 as uuidv1, // semi-persistent client ID | ||
| 6 | v4 as uuidv4 // random ID | ||
| 7 | } from 'uuid'; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Basic telemetry event data. These fields are filled automatically by the `addEvent` call. | ||
| 11 | * @typedef {object} BasicTelemetryEvent | ||
| 12 | * @property {string} clientID - a UUID for this client | ||
| 13 | * @property {string} id - a UUID for this event/packet | ||
| 14 | * @property {string} name - the name of this event (taken from `addEvent`'s `eventName` parameter) | ||
| 15 | * @property {int} timestamp - a Unix epoch timestamp for this event | ||
| 16 | * @property {int} userTimezone - the difference in minutes between UTC and local time | ||
| 17 | */ | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Default telemetry service URLs | ||
| 21 | */ | ||
| 22 | const TelemetryServerURL = Object.freeze({ | ||
| 23 | staging: '', | ||
| 24 | production: 'https://telemetry.openblock.cc/' | ||
| 25 | }); | ||
| 26 | const DefaultServerURL = ( | ||
| 27 | process.env.NODE_ENV === 'production' ? TelemetryServerURL.production : TelemetryServerURL.staging | ||
| 28 | ); | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Default name for persistent configuration & queue storage | ||
| 32 | */ | ||
| 33 | const DefaultStoreName = 'telemetry'; | ||
| 34 | |||
| 35 | /** | ||
| 36 | * Default interval, in seconds, between delivery attempts | ||
| 37 | */ | ||
| 38 | const DefaultDeliveryInterval = 60; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Default interval, in seconds, between connectivity checks | ||
| 42 | */ | ||
| 43 | const DefaultNetworkCheckInterval = 300; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Default limit on the number of queued events | ||
| 47 | */ | ||
| 48 | const DefaultQueueLimit = 100; | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Default limit on the number of delivery attempts for each event | ||
| 52 | */ | ||
| 53 | const DeliveryAttemptLimit = 3; | ||
| 54 | |||
| 55 | const platform = [ | ||
| 56 | `${os.platform()} ${os.release()}`, // "win32 10.0.18362", "darwin 18.7.0", etc. | ||
| 57 | `Electron ${process.versions.electron}`, // "Electron 4.2.6" | ||
| 58 | `Store=${process.mas || process.windowsStore || false}` // "Store=true" or "Store=false" | ||
| 59 | ].join(', '); | ||
| 60 | |||
| 61 | |||
| 62 | /** | ||
| 63 | * Client interface for the Scratch telemetry service. | ||
| 64 | * | ||
| 65 | * This class supports delivering generic telemetry events and is designed to be used by any application or service | ||
| 66 | * in the Scratch family. | ||
| 67 | */ | ||
| 68 | class TelemetryClient { | ||
| 69 | /** | ||
| 70 | * Construct and initialize a TelemetryClient instance, optionally overriding configuration defaults. Delivery | ||
| 71 | * intervals will begin immediately; if the user has not opted in events will be dropped each interval. | ||
| 72 | * | ||
| 73 | * @param {object} [options] - optional configuration settings for this client | ||
| 74 | * @property {string} [storeName] - optional name for persistent config/queue storage (default: 'telemetry') | ||
| 75 | * @property {string} [clientId] - optional UUID for this client (default: automatically determine a UUID) | ||
| 76 | * @property {string} [serverURL] - optional telemetry service endpoint URL (default: automatically choose a server) | ||
| 77 | * @property {boolean} [didOptIn] - optional flag for whether the user opted into telemetry service (default: false) | ||
| 78 | * @property {int} [deliveryInterval] - optional number of seconds between delivery attempts (default: 60) | ||
| 79 | * @property {int} [networkCheckInterval] - optional number of seconds between connectivity checks (default: 300) | ||
| 80 | * @property {int} [queueLimit] - optional limit on the number of queued events (default: 100) | ||
| 81 | * @property {int} [deliveryAttemptLimit] - optional limit on delivery attempts for each event (default: 3) | ||
| 82 | */ | ||
| 83 | constructor ({ | ||
| 84 | storeName = DefaultStoreName, | ||
| 85 | clientID, // undefined = load or create | ||
| 86 | serverURL, // undefined = automatic | ||
| 87 | didOptIn, // undefined = show prompt | ||
| 88 | deliveryInterval = DefaultDeliveryInterval, | ||
| 89 | networkCheckInterval = DefaultNetworkCheckInterval, | ||
| 90 | queueLimit = DefaultQueueLimit, | ||
| 91 | deliveryAttemptLimit = DeliveryAttemptLimit | ||
| 92 | } = {}) { | ||
| 93 | /** | ||
| 94 | * Persistent storage for the client ID, opt in flag, and packet queue. | ||
| 95 | */ | ||
| 96 | this._store = new ElectronStore({ | ||
| 97 | name: storeName | ||
| 98 | }); | ||
| 99 | |||
| 100 | if (clientID) { | ||
| 101 | this.clientID = clientID; | ||
| 102 | } else if (!this._store.has('clientID')) { | ||
| 103 | this.clientID = uuidv1(); | ||
| 104 | } | ||
| 105 | |||
| 106 | if (typeof didOptIn !== 'undefined') { | ||
| 107 | this.didOptIn = didOptIn; | ||
| 108 | } | ||
| 109 | |||
| 110 | /** | ||
| 111 | * Queue for outgoing event packets | ||
| 112 | */ | ||
| 113 | this._packetQueue = this._store.get('packetQueue', []); | ||
| 114 | |||
| 115 | /** | ||
| 116 | * Server URL | ||
| 117 | */ | ||
| 118 | this._serverURL = serverURL || DefaultServerURL; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * Can we currently reach the telemetry service? | ||
| 122 | */ | ||
| 123 | this._networkIsOnline = false; | ||
| 124 | |||
| 125 | /** | ||
| 126 | * Try to deliver telemetry packets at this interval | ||
| 127 | */ | ||
| 128 | this._deliveryInterval = (deliveryInterval > 0) ? deliveryInterval : DefaultDeliveryInterval; | ||
| 129 | |||
| 130 | /** | ||
| 131 | * Check for connectivity at this interval | ||
| 132 | */ | ||
| 133 | this._networkCheckInterval = (networkCheckInterval > 0) ? networkCheckInterval : DefaultNetworkCheckInterval; | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Queue at most this many events | ||
| 137 | */ | ||
| 138 | this._queueLimit = (queueLimit > 0) ? queueLimit : DefaultQueueLimit; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Attempt to deliver an event at most this many times | ||
| 142 | */ | ||
| 143 | this._deliveryAttemptLimit = (deliveryAttemptLimit > 0) ? deliveryAttemptLimit : DeliveryAttemptLimit; | ||
| 144 | |||
| 145 | /** | ||
| 146 | * Bind event handlers | ||
| 147 | */ | ||
| 148 | this._attemptDelivery = this._attemptDelivery.bind(this); | ||
| 149 | this._updateNetworkStatus = this._updateNetworkStatus.bind(this); | ||
| 150 | |||
| 151 | /** | ||
| 152 | * Begin monitoring network status | ||
| 153 | */ | ||
| 154 | this._networkTimer = setInterval(this._updateNetworkStatus, this._networkCheckInterval * 1000); | ||
| 155 | setTimeout(this._updateNetworkStatus, 0); | ||
| 156 | |||
| 157 | /** | ||
| 158 | * Begin the delivery interval | ||
| 159 | */ | ||
| 160 | this._deliveryTimer = setInterval(this._attemptDelivery, this._deliveryInterval * 1000); | ||
| 161 | } | ||
| 162 | |||
| 163 | /** | ||
| 164 | * Stop this client. Do not use this object after disposal. | ||
| 165 | */ | ||
| 166 | dispose () { | ||
| 167 | if (this._networkTimer !== null) { | ||
| 168 | clearInterval(this._networkTimer); | ||
| 169 | this._networkTimer = null; | ||
| 170 | } | ||
| 171 | if (this._deliveryTimer !== null) { | ||
| 172 | clearInterval(this._deliveryTimer); | ||
| 173 | this._deliveryTimer = null; | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | /** | ||
| 178 | * Has the user explicitly opted into this service? | ||
| 179 | * @type {boolean} | ||
| 180 | */ | ||
| 181 | get didOptIn () { | ||
| 182 | // don't supply a default here: we want to track "opt out" separately from "undecided" | ||
| 183 | return this._store.get('optIn'); | ||
| 184 | } | ||
| 185 | set didOptIn (value) { | ||
| 186 | this._store.set('optIn', !!value); | ||
| 187 | } | ||
| 188 | |||
| 189 | /** | ||
| 190 | * Semi-persistent unique ID for this client | ||
| 191 | * @type {string} | ||
| 192 | */ | ||
| 193 | get clientID () { | ||
| 194 | return this._store.get('clientID'); | ||
| 195 | } | ||
| 196 | set clientID (value) { | ||
| 197 | this._store.set('clientID', value); | ||
| 198 | } | ||
| 199 | |||
| 200 | /** | ||
| 201 | * Save the packet queue to the config store. | ||
| 202 | * Call this any time the queue is modified. | ||
| 203 | */ | ||
| 204 | saveQueue () { | ||
| 205 | this._store.set('packetQueue', this._packetQueue); | ||
| 206 | } | ||
| 207 | |||
| 208 | /** | ||
| 209 | * Add an event to the telemetry system. If the user has opted into the telemetry service, this event will be | ||
| 210 | * delivered to the telemetry service when possible. Otherwise the event will be ignored. | ||
| 211 | * | ||
| 212 | * @see {@link BasicTelemetryEvent} for the list of fields which are filled automatically by this method. | ||
| 213 | * | ||
| 214 | * @param {string} eventName - the name of this telemetry event, such as 'app::open'. | ||
| 215 | * @param {object} additionalFields - optional event fields to add or override before sending the event. | ||
| 216 | */ | ||
| 217 | addEvent (eventName, additionalFields = null) { | ||
| 218 | const packetId = uuidv4(); | ||
| 219 | const now = new Date(); | ||
| 220 | |||
| 221 | const packet = Object.assign({ | ||
| 222 | clientID: this.clientID, | ||
| 223 | id: packetId, | ||
| 224 | name: eventName, | ||
| 225 | platform, | ||
| 226 | timestamp: now.getTime(), | ||
| 227 | userTimezone: now.getTimezoneOffset() | ||
| 228 | }, additionalFields); | ||
| 229 | const packetInfo = { | ||
| 230 | attempts: 0, | ||
| 231 | packet | ||
| 232 | }; | ||
| 233 | this._packetQueue.push(packetInfo); | ||
| 234 | this._packetQueue.splice(0, this._packetQueue.length - this._queueLimit); // enforce queue length limit | ||
| 235 | this.saveQueue(); | ||
| 236 | } | ||
| 237 | |||
| 238 | /** | ||
| 239 | * Attempt to deliver events to the telemetry service. If telemetry is disabled, this will do nothing. | ||
| 240 | */ | ||
| 241 | _attemptDelivery () { | ||
| 242 | if (this._busy) { | ||
| 243 | return; | ||
| 244 | } | ||
| 245 | |||
| 246 | /** | ||
| 247 | * Attempt to deliver one event then asynchronously recurse, reenqueueing the event if delivery fails and the | ||
| 248 | * event has not yet reached its retry limit. Sets `this._busy` before doing anything else and clears it once | ||
| 249 | * the queue is empty or `this.didOptIn` is cleared. | ||
| 250 | */ | ||
| 251 | const stepDelivery = () => { | ||
| 252 | this._busy = true; | ||
| 253 | if (!this.didOptIn || !this._networkIsOnline || this._packetQueue.length < 1) { | ||
| 254 | this._busy = false; | ||
| 255 | return; | ||
| 256 | } | ||
| 257 | // don't saveQueue() here: | ||
| 258 | // - if the app exits or crashes before the network request finishes, we'll lose the packet | ||
| 259 | // - if the request finishes, we'll save at that time (see below) | ||
| 260 | const packetInfo = this._packetQueue.shift(); | ||
| 261 | ++packetInfo.attempts; | ||
| 262 | const packet = packetInfo.packet; | ||
| 263 | nets({ | ||
| 264 | body: JSON.stringify(packet), | ||
| 265 | headers: {'Content-Type': 'application/json'}, | ||
| 266 | method: 'POST', | ||
| 267 | url: this._serverURL | ||
| 268 | }, (err, res) => { | ||
| 269 | // TODO: check if the failure is because there's no Internet connection and if so refund the attempt | ||
| 270 | const packetFailed = err || (res.statusCode !== 200); | ||
| 271 | if (packetFailed) { | ||
| 272 | if (packetInfo.attempts < this._deliveryAttemptLimit) { | ||
| 273 | this._packetQueue.push(packetInfo); | ||
| 274 | } else { | ||
| 275 | // eslint-disable-next-line no-console | ||
| 276 | console.warn('Dropping packet which exceeded retry limit', packet); | ||
| 277 | } | ||
| 278 | } | ||
| 279 | this.saveQueue(); | ||
| 280 | stepDelivery(); | ||
| 281 | }); | ||
| 282 | }; | ||
| 283 | |||
| 284 | stepDelivery(); | ||
| 285 | } | ||
| 286 | |||
| 287 | /** | ||
| 288 | * Check if the telemetry service is available | ||
| 289 | */ | ||
| 290 | _updateNetworkStatus () { | ||
| 291 | nets({ | ||
| 292 | method: 'GET', | ||
| 293 | url: this._serverURL | ||
| 294 | }, (err, res) => { | ||
| 295 | this._networkIsOnline = !err && (res.statusCode === 200); | ||
| 296 | }); | ||
| 297 | } | ||
| 298 | } | ||
| 299 | |||
| 300 | export default TelemetryClient; |
src/renderer/.eslintrc.js
0 → 100644
src/renderer/ScratchDesktopAppStateHOC.jsx
0 → 100644
| 1 | import {ipcRenderer} from 'electron'; | ||
| 2 | import bindAll from 'lodash.bindall'; | ||
| 3 | import React from 'react'; | ||
| 4 | |||
| 5 | /** | ||
| 6 | * Higher-order component to add desktop logic to AppStateHOC. | ||
| 7 | * @param {Component} WrappedComponent - an AppStateHOC-like component to wrap. | ||
| 8 | * @returns {Component} - a component similar to AppStateHOC with desktop-specific logic added. | ||
| 9 | */ | ||
| 10 | const ScratchDesktopAppStateHOC = function (WrappedComponent) { | ||
| 11 | class ScratchDesktopAppStateComponent extends React.Component { | ||
| 12 | constructor (props) { | ||
| 13 | super(props); | ||
| 14 | bindAll(this, [ | ||
| 15 | 'handleTelemetryModalOptIn', | ||
| 16 | 'handleTelemetryModalOptOut' | ||
| 17 | ]); | ||
| 18 | this.state = { | ||
| 19 | // use `sendSync` because this should be set before first render | ||
| 20 | telemetryDidOptIn: ipcRenderer.sendSync('getTelemetryDidOptIn') | ||
| 21 | }; | ||
| 22 | } | ||
| 23 | handleTelemetryModalOptIn () { | ||
| 24 | ipcRenderer.send('setTelemetryDidOptIn', true); | ||
| 25 | ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { | ||
| 26 | this.setState({telemetryDidOptIn}); | ||
| 27 | }); | ||
| 28 | } | ||
| 29 | handleTelemetryModalOptOut () { | ||
| 30 | ipcRenderer.send('setTelemetryDidOptIn', false); | ||
| 31 | ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { | ||
| 32 | this.setState({telemetryDidOptIn}); | ||
| 33 | }); | ||
| 34 | } | ||
| 35 | render () { | ||
| 36 | const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean'); | ||
| 37 | |||
| 38 | return (<WrappedComponent | ||
| 39 | isTelemetryEnabled={this.state.telemetryDidOptIn} | ||
| 40 | onTelemetryModalOptIn={this.handleTelemetryModalOptIn} | ||
| 41 | onTelemetryModalOptOut={this.handleTelemetryModalOptOut} | ||
| 42 | showTelemetryModal={shouldShowTelemetryModal} | ||
| 43 | |||
| 44 | // allow passed-in props to override any of the above | ||
| 45 | {...this.props} | ||
| 46 | />); | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | return ScratchDesktopAppStateComponent; | ||
| 51 | }; | ||
| 52 | |||
| 53 | export default ScratchDesktopAppStateHOC; |
src/renderer/ScratchDesktopGUIHOC.jsx
0 → 100644
| 1 | import {ipcRenderer} from 'electron'; | ||
| 2 | import {dialog} from '@electron/remote'; | ||
| 3 | import * as remote from '@electron/remote/renderer'; | ||
| 4 | import bindAll from 'lodash.bindall'; | ||
| 5 | import omit from 'lodash.omit'; | ||
| 6 | import PropTypes from 'prop-types'; | ||
| 7 | import React from 'react'; | ||
| 8 | import {connect} from 'react-redux'; | ||
| 9 | import GUIComponent from 'openblock-gui/src/components/gui/gui.jsx'; | ||
| 10 | import {FormattedMessage} from 'react-intl'; | ||
| 11 | |||
| 12 | import { | ||
| 13 | LoadingStates, | ||
| 14 | onFetchedProjectData, | ||
| 15 | onLoadedProject, | ||
| 16 | defaultProjectId, | ||
| 17 | requestNewProject, | ||
| 18 | requestProjectUpload, | ||
| 19 | setProjectId | ||
| 20 | } from 'openblock-gui/src/reducers/project-state'; | ||
| 21 | import { | ||
| 22 | openLoadingProject, | ||
| 23 | closeLoadingProject, | ||
| 24 | openTelemetryModal, | ||
| 25 | openUpdateModal | ||
| 26 | } from 'openblock-gui/src/reducers/modals'; | ||
| 27 | import {setUpdate} from 'openblock-gui/src/reducers/update'; | ||
| 28 | |||
| 29 | import analytics, {initialAnalytics} from 'openblock-gui/src/lib/analytics'; | ||
| 30 | import MessageBoxType from 'openblock-gui/src/lib/message-box.js'; | ||
| 31 | |||
| 32 | import ElectronStorageHelper from '../common/ElectronStorageHelper'; | ||
| 33 | |||
| 34 | import showPrivacyPolicy from './showPrivacyPolicy'; | ||
| 35 | |||
| 36 | /** | ||
| 37 | * Higher-order component to add desktop logic to the GUI. | ||
| 38 | * @param {Component} WrappedComponent - a GUI-like component to wrap. | ||
| 39 | * @returns {Component} - a component similar to GUI with desktop-specific logic added. | ||
| 40 | */ | ||
| 41 | const ScratchDesktopGUIHOC = function (WrappedComponent) { | ||
| 42 | class ScratchDesktopGUIComponent extends React.Component { | ||
| 43 | constructor (props) { | ||
| 44 | super(props); | ||
| 45 | bindAll(this, [ | ||
| 46 | 'handleProjectTelemetryEvent', | ||
| 47 | 'handleSetTitleFromSave', | ||
| 48 | 'handleShowMessageBox', | ||
| 49 | 'handleStorageInit', | ||
| 50 | 'handleUpdateProjectTitle' | ||
| 51 | ]); | ||
| 52 | this.props.onLoadingStarted(); | ||
| 53 | ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => { | ||
| 54 | const hasInitialProject = initialProjectData && (initialProjectData.length > 0); | ||
| 55 | this.props.onHasInitialProject(hasInitialProject, this.props.loadingState); | ||
| 56 | if (!hasInitialProject) { | ||
| 57 | this.props.onLoadingCompleted(); | ||
| 58 | ipcRenderer.send('loading-completed'); | ||
| 59 | return; | ||
| 60 | } | ||
| 61 | this.props.vm.loadProject(initialProjectData).then( | ||
| 62 | () => { | ||
| 63 | this.props.onLoadingCompleted(); | ||
| 64 | ipcRenderer.send('loading-completed'); | ||
| 65 | this.props.onLoadedProject(this.props.loadingState, true); | ||
| 66 | }, | ||
| 67 | e => { | ||
| 68 | this.props.onLoadingCompleted(); | ||
| 69 | ipcRenderer.send('loading-completed'); | ||
| 70 | this.props.onLoadedProject(this.props.loadingState, false); | ||
| 71 | dialog.showMessageBox(remote.getCurrentWindow(), { | ||
| 72 | type: 'error', | ||
| 73 | title: 'Failed to load project', | ||
| 74 | message: 'Invalid or corrupt project file.', | ||
| 75 | detail: e.message | ||
| 76 | }); | ||
| 77 | |||
| 78 | // this effectively sets the default project ID | ||
| 79 | // TODO: maybe setting the default project ID should be implicit in `requestNewProject` | ||
| 80 | this.props.onHasInitialProject(false, this.props.loadingState); | ||
| 81 | |||
| 82 | // restart as if we didn't have an initial project to load | ||
| 83 | this.props.onRequestNewProject(); | ||
| 84 | } | ||
| 85 | ); | ||
| 86 | }); | ||
| 87 | ipcRenderer.send('set-locale', this.props.locale); | ||
| 88 | } | ||
| 89 | componentDidMount () { | ||
| 90 | ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave); | ||
| 91 | ipcRenderer.on('setUpdate', (event, args) => { | ||
| 92 | this.props.onSetUpdate(args); | ||
| 93 | }); | ||
| 94 | ipcRenderer.on('setUserId', (event, args) => { | ||
| 95 | initialAnalytics(args); | ||
| 96 | // Register "base" page view | ||
| 97 | analytics.send({hitType: 'pageview', page: '/community/electron'}); | ||
| 98 | }); | ||
| 99 | ipcRenderer.on('setPlatform', (event, args) => { | ||
| 100 | this.platform = args; | ||
| 101 | }); | ||
| 102 | } | ||
| 103 | componentWillUnmount () { | ||
| 104 | ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave); | ||
| 105 | } | ||
| 106 | handleClickAbout () { | ||
| 107 | ipcRenderer.send('open-about-window'); | ||
| 108 | } | ||
| 109 | handleClickLicense () { | ||
| 110 | ipcRenderer.send('open-license-window'); | ||
| 111 | } | ||
| 112 | handleClickCheckUpdate () { | ||
| 113 | ipcRenderer.send('reqeustCheckUpdate'); | ||
| 114 | } | ||
| 115 | handleClickUpdate () { | ||
| 116 | ipcRenderer.send('reqeustUpdate'); | ||
| 117 | } | ||
| 118 | handleAbortUpdate () { | ||
| 119 | ipcRenderer.send('abortUpdate'); | ||
| 120 | } | ||
| 121 | handleClickClearCache () { | ||
| 122 | ipcRenderer.send('clearCache'); | ||
| 123 | } | ||
| 124 | handleClickInstallDriver () { | ||
| 125 | ipcRenderer.send('installDriver'); | ||
| 126 | } | ||
| 127 | handleProjectTelemetryEvent (event, metadata) { | ||
| 128 | ipcRenderer.send(event, metadata); | ||
| 129 | } | ||
| 130 | handleSetTitleFromSave (event, args) { | ||
| 131 | this.handleUpdateProjectTitle(args.title); | ||
| 132 | } | ||
| 133 | handleStorageInit (storageInstance) { | ||
| 134 | storageInstance.addHelper(new ElectronStorageHelper(storageInstance)); | ||
| 135 | } | ||
| 136 | handleUpdateProjectTitle (newTitle) { | ||
| 137 | this.setState({projectTitle: newTitle}); | ||
| 138 | } | ||
| 139 | handleShowMessageBox (type, message) { | ||
| 140 | /** | ||
| 141 | * To avoid the electron bug: the input-box lose focus after call alert or confirm on windows platform. | ||
| 142 | * https://github.com/electron/electron/issues/19977 | ||
| 143 | */ | ||
| 144 | if (this.platform === 'win32') { | ||
| 145 | let options; | ||
| 146 | if (type === MessageBoxType.confirm) { | ||
| 147 | options = { | ||
| 148 | type: 'warning', | ||
| 149 | buttons: ['Ok', 'Cancel'], | ||
| 150 | message: message | ||
| 151 | }; | ||
| 152 | } else if (type === MessageBoxType.alert) { | ||
| 153 | options = { | ||
| 154 | type: 'error', | ||
| 155 | message: message | ||
| 156 | }; | ||
| 157 | } | ||
| 158 | const result = dialog.showMessageBoxSync(remote.getCurrentWindow(), options); | ||
| 159 | if (result === 0) { | ||
| 160 | return true; | ||
| 161 | } | ||
| 162 | return false; | ||
| 163 | } | ||
| 164 | if (type === 'confirm') { | ||
| 165 | return confirm(message); // eslint-disable-line no-alert | ||
| 166 | } | ||
| 167 | return alert(message); // eslint-disable-line no-alert | ||
| 168 | } | ||
| 169 | render () { | ||
| 170 | const childProps = omit(this.props, Object.keys(ScratchDesktopGUIComponent.propTypes)); | ||
| 171 | |||
| 172 | return (<WrappedComponent | ||
| 173 | canEditTitle | ||
| 174 | canModifyCloudData={false} | ||
| 175 | canSave={false} | ||
| 176 | isScratchDesktop | ||
| 177 | onClickAbout={[ | ||
| 178 | { | ||
| 179 | title: (<FormattedMessage | ||
| 180 | defaultMessage="About" | ||
| 181 | description="Menu bar item for about" | ||
| 182 | id="gui.desktopMenuBar.about" | ||
| 183 | />), | ||
| 184 | onClick: () => this.handleClickAbout() | ||
| 185 | }, | ||
| 186 | { | ||
| 187 | title: (<FormattedMessage | ||
| 188 | defaultMessage="License" | ||
| 189 | description="Menu bar item for license" | ||
| 190 | id="gui.desktopMenuBar.license" | ||
| 191 | />), | ||
| 192 | onClick: () => this.handleClickLicense() | ||
| 193 | }, | ||
| 194 | { | ||
| 195 | title: (<FormattedMessage | ||
| 196 | defaultMessage="Privacy policy" | ||
| 197 | description="Menu bar item for privacy policy" | ||
| 198 | id="gui.menuBar.privacyPolicy" | ||
| 199 | />), | ||
| 200 | onClick: () => showPrivacyPolicy() | ||
| 201 | }, | ||
| 202 | { | ||
| 203 | title: (<FormattedMessage | ||
| 204 | defaultMessage="Data settings" | ||
| 205 | description="Menu bar item for data settings" | ||
| 206 | id="gui.menuBar.dataSettings" | ||
| 207 | />), | ||
| 208 | onClick: () => this.props.onTelemetrySettingsClicked() | ||
| 209 | } | ||
| 210 | ]} | ||
| 211 | onClickLogo={this.handleClickLogo} | ||
| 212 | onClickCheckUpdate={this.handleClickCheckUpdate} | ||
| 213 | onClickUpdate={this.handleClickUpdate} | ||
| 214 | onAbortUpdate={this.handleAbortUpdate} | ||
| 215 | onClickInstallDriver={this.handleClickInstallDriver} | ||
| 216 | onClickClearCache={this.handleClickClearCache} | ||
| 217 | onProjectTelemetryEvent={this.handleProjectTelemetryEvent} | ||
| 218 | onShowMessageBox={this.handleShowMessageBox} | ||
| 219 | onShowPrivacyPolicy={showPrivacyPolicy} | ||
| 220 | onStorageInit={this.handleStorageInit} | ||
| 221 | onUpdateProjectTitle={this.handleUpdateProjectTitle} | ||
| 222 | |||
| 223 | // allow passed-in props to override any of the above | ||
| 224 | {...childProps} | ||
| 225 | />); | ||
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 229 | ScratchDesktopGUIComponent.propTypes = { | ||
| 230 | loadingState: PropTypes.oneOf(LoadingStates), | ||
| 231 | locale: PropTypes.string.isRequired, | ||
| 232 | onFetchedInitialProjectData: PropTypes.func, | ||
| 233 | onHasInitialProject: PropTypes.func, | ||
| 234 | onLoadedProject: PropTypes.func, | ||
| 235 | onLoadingCompleted: PropTypes.func, | ||
| 236 | onLoadingStarted: PropTypes.func, | ||
| 237 | onRequestNewProject: PropTypes.func, | ||
| 238 | onTelemetrySettingsClicked: PropTypes.func, | ||
| 239 | onSetUpdate: PropTypes.func, | ||
| 240 | // using PropTypes.instanceOf(VM) here will cause prop type warnings due to VM mismatch | ||
| 241 | vm: GUIComponent.WrappedComponent.propTypes.vm | ||
| 242 | }; | ||
| 243 | const mapStateToProps = state => { | ||
| 244 | const loadingState = state.scratchGui.projectState.loadingState; | ||
| 245 | return { | ||
| 246 | loadingState: loadingState, | ||
| 247 | locale: state.locales.locale, | ||
| 248 | vm: state.scratchGui.vm | ||
| 249 | }; | ||
| 250 | }; | ||
| 251 | const mapDispatchToProps = dispatch => ({ | ||
| 252 | onLoadingStarted: () => dispatch(openLoadingProject()), | ||
| 253 | onLoadingCompleted: () => dispatch(closeLoadingProject()), | ||
| 254 | onHasInitialProject: (hasInitialProject, loadingState) => { | ||
| 255 | if (hasInitialProject) { | ||
| 256 | // emulate sb-file-uploader | ||
| 257 | return dispatch(requestProjectUpload(loadingState)); | ||
| 258 | } | ||
| 259 | |||
| 260 | // `createProject()` might seem more appropriate but it's not a valid state transition here | ||
| 261 | // setting the default project ID is a valid transition from NOT_LOADED and acts like "create new" | ||
| 262 | return dispatch(setProjectId(defaultProjectId)); | ||
| 263 | }, | ||
| 264 | onFetchedInitialProjectData: (projectData, loadingState) => | ||
| 265 | dispatch(onFetchedProjectData(projectData, loadingState)), | ||
| 266 | onLoadedProject: (loadingState, loadSuccess) => { | ||
| 267 | const canSaveToServer = false; | ||
| 268 | return dispatch(onLoadedProject(loadingState, canSaveToServer, loadSuccess)); | ||
| 269 | }, | ||
| 270 | onRequestNewProject: () => dispatch(requestNewProject(false)), | ||
| 271 | onSetUpdate: arg => { | ||
| 272 | dispatch(setUpdate(arg)); | ||
| 273 | dispatch(openUpdateModal()); | ||
| 274 | }, | ||
| 275 | onTelemetrySettingsClicked: () => dispatch(openTelemetryModal()) | ||
| 276 | }); | ||
| 277 | |||
| 278 | return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopGUIComponent); | ||
| 279 | }; | ||
| 280 | |||
| 281 | export default ScratchDesktopGUIHOC; |
src/renderer/about.css
0 → 100644
| 1 | html, body { | ||
| 2 | background-color: #4D97FF; | ||
| 3 | color: white; | ||
| 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | ||
| 5 | font-weight: bolder; | ||
| 6 | } | ||
| 7 | |||
| 8 | a:active, a:hover, a:link, a:visited { | ||
| 9 | color: currentColor; | ||
| 10 | } | ||
| 11 | |||
| 12 | a:active, a:hover { | ||
| 13 | filter: brightness(0.9); | ||
| 14 | } | ||
| 15 | |||
| 16 | .aboutBox { | ||
| 17 | margin: 0; | ||
| 18 | position: absolute; | ||
| 19 | top: 50%; | ||
| 20 | left: 50%; | ||
| 21 | transform: translate(-50%, -50%); | ||
| 22 | text-align:center; | ||
| 23 | } | ||
| 24 | |||
| 25 | .aboutLogo { | ||
| 26 | max-width: 10rem; | ||
| 27 | max-height: 10rem; | ||
| 28 | margin: 1.5rem 1.5rem 0 1.5rem; | ||
| 29 | } | ||
| 30 | |||
| 31 | .aboutText { | ||
| 32 | margin: 1.5rem; | ||
| 33 | } | ||
| 34 | |||
| 35 | .aboutDetails { | ||
| 36 | font-size: x-small; | ||
| 37 | text-align:left; | ||
| 38 | margin:auto; | ||
| 39 | |||
| 40 | } | ||
| 41 | |||
| 42 | .aboutFooter { | ||
| 43 | font-size: small; | ||
| 44 | } |
src/renderer/about.jsx
0 → 100644
| 1 | import React from 'react'; | ||
| 2 | import {productName, version} from '../../package.json'; | ||
| 3 | |||
| 4 | import logo from '../icon/OpenBlockDesktop.svg'; | ||
| 5 | import styles from './about.css'; | ||
| 6 | |||
| 7 | const AboutElement = () => ( | ||
| 8 | <div className={styles.aboutBox}> | ||
| 9 | <div><img | ||
| 10 | alt={`${productName} icon`} | ||
| 11 | src={logo} | ||
| 12 | className={styles.aboutLogo} | ||
| 13 | /></div> | ||
| 14 | <div className={styles.aboutText}> | ||
| 15 | <h2>{productName}</h2> | ||
| 16 | Version {version} | ||
| 17 | <table className={styles.aboutDetails}><tbody> | ||
| 18 | { | ||
| 19 | ['Electron', 'Chrome', 'Node'].map(component => { | ||
| 20 | const componentVersion = process.versions[component.toLowerCase()]; | ||
| 21 | return <tr key={component}><td>{component}</td><td>{componentVersion}</td></tr>; | ||
| 22 | }) | ||
| 23 | } | ||
| 24 | </tbody></table> | ||
| 25 | </div> | ||
| 26 | </div> | ||
| 27 | ); | ||
| 28 | |||
| 29 | export default <AboutElement />; |
src/renderer/app.css
0 → 100644
src/renderer/app.jsx
0 → 100644
| 1 | import React from 'react'; | ||
| 2 | import {compose} from 'redux'; | ||
| 3 | import GUI from 'openblock-gui/src/index'; | ||
| 4 | |||
| 5 | import AppStateHOC from 'openblock-gui/src/lib/app-state-hoc.jsx'; | ||
| 6 | |||
| 7 | import ScratchDesktopAppStateHOC from './ScratchDesktopAppStateHOC.jsx'; | ||
| 8 | import ScratchDesktopGUIHOC from './ScratchDesktopGUIHOC.jsx'; | ||
| 9 | import styles from './app.css'; | ||
| 10 | |||
| 11 | const appTarget = document.getElementById('app'); | ||
| 12 | appTarget.className = styles.app || 'app'; | ||
| 13 | |||
| 14 | GUI.setAppElement(appTarget); | ||
| 15 | |||
| 16 | |||
| 17 | // note that redux's 'compose' function is just being used as a general utility to make | ||
| 18 | // the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's | ||
| 19 | // ability to compose reducers. | ||
| 20 | const WrappedGui = compose( | ||
| 21 | ScratchDesktopAppStateHOC, | ||
| 22 | AppStateHOC, | ||
| 23 | ScratchDesktopGUIHOC | ||
| 24 | )(GUI); | ||
| 25 | |||
| 26 | export default <WrappedGui />; |
src/renderer/index.html
0 → 100644
| 1 | <!DOCTYPE html> | ||
| 2 | <html> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"> | ||
| 5 | <style> | ||
| 6 | body { | ||
| 7 | background-color: #4D97FF; | ||
| 8 | } | ||
| 9 | .splash { | ||
| 10 | color: white; | ||
| 11 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | ||
| 12 | font-size: xx-large; | ||
| 13 | font-weight: bolder; | ||
| 14 | margin: 0; | ||
| 15 | position: absolute; | ||
| 16 | top: 50%; | ||
| 17 | left: 50%; | ||
| 18 | transform: translate(-50%, -50%); | ||
| 19 | } | ||
| 20 | </style> | ||
| 21 | </head> | ||
| 22 | <body> | ||
| 23 | <div id="app"><p class="splash">OpenBlock is loading...</p></div> | ||
| 24 | </body> | ||
| 25 | </html> |
src/renderer/index.js
0 → 100644
| 1 | // This file does async imports of the heavy JSX, especially app.jsx, to avoid blocking the first render. | ||
| 2 | // The main index.html just contains a loading/splash screen which will display while this import loads. | ||
| 3 | |||
| 4 | import {ipcRenderer} from 'electron'; | ||
| 5 | |||
| 6 | import ReactDOM from 'react-dom'; | ||
| 7 | |||
| 8 | ipcRenderer.on('ready-to-show', () => { | ||
| 9 | // Start without any element in focus, otherwise the first link starts with focus and shows an orange box. | ||
| 10 | // We shouldn't disable that box or the focus behavior in case someone wants or needs to navigate that way. | ||
| 11 | // This seems like a hack... maybe there's some better way to do avoid any element starting with focus? | ||
| 12 | document.activeElement.blur(); | ||
| 13 | }); | ||
| 14 | |||
| 15 | const route = new URLSearchParams(window.location.search).get('route') || 'app'; | ||
| 16 | let routeModulePromise; | ||
| 17 | switch (route) { | ||
| 18 | case 'loading': | ||
| 19 | routeModulePromise = import('./loading.jsx'); | ||
| 20 | break; | ||
| 21 | case 'app': | ||
| 22 | routeModulePromise = import('./app.jsx'); | ||
| 23 | break; | ||
| 24 | case 'about': | ||
| 25 | routeModulePromise = import('./about.jsx'); | ||
| 26 | break; | ||
| 27 | case 'license': | ||
| 28 | routeModulePromise = import('./license.jsx'); | ||
| 29 | break; | ||
| 30 | case 'privacy': | ||
| 31 | routeModulePromise = import('./privacy.jsx'); | ||
| 32 | break; | ||
| 33 | } | ||
| 34 | |||
| 35 | routeModulePromise.then(routeModule => { | ||
| 36 | const appTarget = document.getElementById('app'); | ||
| 37 | const routeElement = routeModule.default; | ||
| 38 | ReactDOM.render(routeElement, appTarget); | ||
| 39 | }); |
src/renderer/license.css
0 → 100644
| 1 | html, body { | ||
| 2 | background-color: #4D97FF; | ||
| 3 | color: white; | ||
| 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | ||
| 5 | font-weight: normal; | ||
| 6 | line-height: 150%; | ||
| 7 | } | ||
| 8 | |||
| 9 | a:active, a:hover, a:link, a:visited { | ||
| 10 | color: currentColor; | ||
| 11 | } | ||
| 12 | |||
| 13 | a:active, a:hover { | ||
| 14 | filter: brightness(0.9); | ||
| 15 | } | ||
| 16 | |||
| 17 | .licenseBox { | ||
| 18 | margin: 3rem; | ||
| 19 | position: absolute; | ||
| 20 | } | ||
| 21 | |||
| 22 | .aboutFooter { | ||
| 23 | font-size: small; | ||
| 24 | } | ||
| 25 | |||
| 26 | .tabList { | ||
| 27 | height: 32px; | ||
| 28 | width: 250px; /* Match width of the toolbox */ | ||
| 29 | display: flex; | ||
| 30 | align-items: flex-end; | ||
| 31 | flex-shrink: 0; | ||
| 32 | |||
| 33 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | ||
| 34 | font-weight: 500; | ||
| 35 | font-size: 0.80rem; | ||
| 36 | |||
| 37 | /* Overrides for react-tabs styling */ | ||
| 38 | margin: 0 !important; | ||
| 39 | border-bottom: 0 !important; | ||
| 40 | } | ||
| 41 | |||
| 42 | .tab { | ||
| 43 | flex-grow: 1; | ||
| 44 | height: 80%; | ||
| 45 | margin-bottom: 0; | ||
| 46 | |||
| 47 | border-radius: 0.5rem 0.5rem 0 0; | ||
| 48 | border: 1px solid hsla(0, 0%, 0%, 0.15); | ||
| 49 | |||
| 50 | padding: 0.125rem 1.25rem 0; | ||
| 51 | font-size: 0.75rem; | ||
| 52 | |||
| 53 | background-color: rgb(219, 219, 219); | ||
| 54 | color: #575e75; | ||
| 55 | |||
| 56 | display: flex; | ||
| 57 | justify-content: center; | ||
| 58 | align-items: center; | ||
| 59 | |||
| 60 | user-select: none; | ||
| 61 | white-space: nowrap; | ||
| 62 | } | ||
| 63 | |||
| 64 | .tab { | ||
| 65 | margin-left: -0.5rem; | ||
| 66 | } | ||
| 67 | |||
| 68 | .tab:nth-of-type(1) { | ||
| 69 | margin-left: 0; | ||
| 70 | } | ||
| 71 | |||
| 72 | /* Use z-indices to force left-on-top for tabs */ | ||
| 73 | .tab:nth-of-type(1) { | ||
| 74 | z-index: 3; | ||
| 75 | } | ||
| 76 | .tab:nth-of-type(2) { | ||
| 77 | z-index: 2; | ||
| 78 | } | ||
| 79 | .tab:nth-of-type(3) { | ||
| 80 | z-index: 1; | ||
| 81 | } | ||
| 82 | |||
| 83 | .tab:hover { | ||
| 84 | background-color: rgb(235, 235, 235); | ||
| 85 | } | ||
| 86 | |||
| 87 | .tab.isSelected { | ||
| 88 | height: 90%; | ||
| 89 | background-color: white; | ||
| 90 | color: #4D97FF; | ||
| 91 | border-bottom: none; | ||
| 92 | z-index: 4; | ||
| 93 | } | ||
| 94 | |||
| 95 | .tab.active { | ||
| 96 | background-color: hsla(215, 100%, 65%, 0.15); | ||
| 97 | } | ||
| 98 | |||
| 99 | .tabs { | ||
| 100 | position: relative; | ||
| 101 | flex-grow: 1; | ||
| 102 | display: flex; | ||
| 103 | flex-direction: column; | ||
| 104 | border: 1px solid #D9D9D9; | ||
| 105 | border-top: none; | ||
| 106 | } | ||
| 107 | |||
| 108 | .tabPanel { | ||
| 109 | position: relative; | ||
| 110 | flex-grow: 1; | ||
| 111 | display: none; | ||
| 112 | background-color: white; | ||
| 113 | color: #575e75; | ||
| 114 | padding: 2rem 3rem; | ||
| 115 | } | ||
| 116 | |||
| 117 | .tabPanel h4{ | ||
| 118 | margin: 0.2rem 0; | ||
| 119 | } | ||
| 120 | |||
| 121 | .tabPanel.isSelected { | ||
| 122 | display: flex; | ||
| 123 | flex-direction: column; | ||
| 124 | } | ||
| 125 | |||
| 126 | .logo { | ||
| 127 | height: 80px; | ||
| 128 | margin: 3rem auto 3rem; | ||
| 129 | } | ||
| 130 | |||
| 131 | .logo:hover { | ||
| 132 | cursor: pointer; | ||
| 133 | } |
src/renderer/license.jsx
0 → 100644
| 1 | /* eslint-disable max-len */ | ||
| 2 | import React from 'react'; | ||
| 3 | import styles from './license.css'; | ||
| 4 | import bindAll from 'lodash.bindall'; | ||
| 5 | import classNames from 'classnames'; | ||
| 6 | |||
| 7 | import OpenBlockLogo from '../icon/logo-OpenBlockcc.svg'; | ||
| 8 | import ScratchFoundationLogo from '../icon/logo-ScratchFoundation.svg'; | ||
| 9 | |||
| 10 | // Insert new copyright information at the head of the array to add a new copyright notice | ||
| 11 | const copyrightInformations = [ | ||
| 12 | { | ||
| 13 | id: 'OpenBlock.cc', | ||
| 14 | logo: OpenBlockLogo, | ||
| 15 | link: 'https://www.openblock.cc/', | ||
| 16 | license: 'MIT' | ||
| 17 | }, | ||
| 18 | { | ||
| 19 | id: 'Scratch Foundation', | ||
| 20 | link: 'https://www.scratchfoundation.org/', | ||
| 21 | logo: ScratchFoundationLogo, | ||
| 22 | license: 'BSD-3-Clause' | ||
| 23 | } | ||
| 24 | ]; | ||
| 25 | |||
| 26 | const licenseContent = { | ||
| 27 | 'MIT': ( | ||
| 28 | <div className={styles.licenseContent}> | ||
| 29 | <p> | ||
| 30 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 31 | of this software and associated documentation files (the "Software"), to deal | ||
| 32 | in the Software without restriction, including without limitation the rights | ||
| 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 34 | copies of the Software, and to permit persons to whom the Software is | ||
| 35 | furnished to do so, subject to the following conditions: | ||
| 36 | </p> | ||
| 37 | <p> | ||
| 38 | The above copyright notice and this permission notice shall be included in all | ||
| 39 | copies or substantial portions of the Software. | ||
| 40 | </p> | ||
| 41 | <p> | ||
| 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 47 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 48 | SOFTWARE. | ||
| 49 | </p> | ||
| 50 | </div> | ||
| 51 | ), | ||
| 52 | 'BSD-3-Clause': ( | ||
| 53 | <div className={styles.licenseContent}> | ||
| 54 | <p> | ||
| 55 | Redistribution and use in source and binary forms, with or without modification, | ||
| 56 | are permitted provided that the following conditions are met: | ||
| 57 | </p> | ||
| 58 | <p> | ||
| 59 | 1. Redistributions of source code must retain the above copyright notice, this | ||
| 60 | list of conditions and the following disclaimer. | ||
| 61 | </p> | ||
| 62 | <p> | ||
| 63 | 2. Redistributions in binary form must reproduce the above copyright notice, this | ||
| 64 | list of conditions and the following disclaimer in the documentation and/or other | ||
| 65 | materials provided with the distribution. | ||
| 66 | </p> | ||
| 67 | <p> | ||
| 68 | 3. Neither the name of the copyright holder nor the names of its contributors may be | ||
| 69 | used to endorse or promote products derived from this software without specific | ||
| 70 | prior written permission. | ||
| 71 | </p> | ||
| 72 | <p> | ||
| 73 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY | ||
| 74 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | ||
| 75 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT | ||
| 76 | SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | ||
| 77 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | ||
| 78 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; | ||
| 79 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | ||
| 80 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING | ||
| 81 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | ||
| 82 | SUCH DAMAGE. | ||
| 83 | </p> | ||
| 84 | </div> | ||
| 85 | ) | ||
| 86 | }; | ||
| 87 | |||
| 88 | class LicenseElement extends React.Component { | ||
| 89 | constructor (props) { | ||
| 90 | super(props); | ||
| 91 | bindAll(this, [ | ||
| 92 | 'handleClickTab' | ||
| 93 | ]); | ||
| 94 | this.state = { | ||
| 95 | selectedTab: copyrightInformations[0].id | ||
| 96 | }; | ||
| 97 | } | ||
| 98 | |||
| 99 | handleClickLogo (e) { | ||
| 100 | copyrightInformations.forEach(item => { | ||
| 101 | if (item.id === e.currentTarget.alt) { | ||
| 102 | window.open(item.link); | ||
| 103 | } | ||
| 104 | }); | ||
| 105 | } | ||
| 106 | |||
| 107 | handleClickTab (e) { | ||
| 108 | this.setState({selectedTab: e.currentTarget.id}); | ||
| 109 | } | ||
| 110 | |||
| 111 | buildLicenseTabList () { | ||
| 112 | return copyrightInformations.map(item => ( | ||
| 113 | <button | ||
| 114 | key={item.id} | ||
| 115 | id={item.id} | ||
| 116 | className={classNames(styles.tab, { | ||
| 117 | [styles.isSelected]: this.state.selectedTab === item.id | ||
| 118 | })} | ||
| 119 | onClick={this.handleClickTab} | ||
| 120 | > | ||
| 121 | {item.id} | ||
| 122 | </button> | ||
| 123 | )); | ||
| 124 | } | ||
| 125 | |||
| 126 | buildLicenseContent () { | ||
| 127 | return copyrightInformations.map(item => ( | ||
| 128 | <div | ||
| 129 | key={item.id} | ||
| 130 | className={classNames(styles.tabPanel, { | ||
| 131 | [styles.isSelected]: this.state.selectedTab === item.id | ||
| 132 | })} | ||
| 133 | > | ||
| 134 | <img | ||
| 135 | alt={item.id} | ||
| 136 | className={styles.logo} | ||
| 137 | draggable={false} | ||
| 138 | src={item.logo} | ||
| 139 | onClick={this.handleClickLogo} | ||
| 140 | /> | ||
| 141 | <h4>{item.license} License</h4> | ||
| 142 | <h4>Copyright © {item.id}</h4> | ||
| 143 | {licenseContent[item.license]} | ||
| 144 | </div> | ||
| 145 | )); | ||
| 146 | } | ||
| 147 | |||
| 148 | render () { | ||
| 149 | const tabList = this.buildLicenseTabList(); | ||
| 150 | const content = this.buildLicenseContent(); | ||
| 151 | |||
| 152 | return ( | ||
| 153 | <div className={styles.licenseBox}> | ||
| 154 | <div className={styles.tabList}> | ||
| 155 | {tabList} | ||
| 156 | </div> | ||
| 157 | <div className={styles.tabs}> | ||
| 158 | {content} | ||
| 159 | </div> | ||
| 160 | </div> | ||
| 161 | ); | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | export default <LicenseElement />; |
src/renderer/loading.css
0 → 100644
src/renderer/loading.jsx
0 → 100644
| 1 | import React from 'react'; | ||
| 2 | import {productName} from '../../package.json'; | ||
| 3 | |||
| 4 | import logo from '../icon/OpenBlockLoading.svg'; | ||
| 5 | import styles from './loading.css'; | ||
| 6 | |||
| 7 | const LoadingElement = () => ( | ||
| 8 | <div className={styles.loadingBox}> | ||
| 9 | <div> | ||
| 10 | <img | ||
| 11 | alt={`${productName} loading icon`} | ||
| 12 | src={logo} | ||
| 13 | className={styles.loadingLogo} | ||
| 14 | /> | ||
| 15 | </div> | ||
| 16 | </div> | ||
| 17 | ); | ||
| 18 | |||
| 19 | export default <LoadingElement />; |
src/renderer/privacy.css
0 → 100644
src/renderer/privacy.jsx
0 → 100644
| 1 | /* eslint-disable max-len */ | ||
| 2 | import React from 'react'; | ||
| 3 | |||
| 4 | import styles from './privacy.css'; | ||
| 5 | |||
| 6 | const PrivacyElement = () => ( | ||
| 7 | <div className={styles.privacyBox}> | ||
| 8 | <h1>Privacy Policy</h1> | ||
| 9 | <i>The OpenBlock Privacy Policy was last updated: October 5, 2020</i> | ||
| 10 | <p> | ||
| 11 | The OpenBlock Team (“OpenBlock”, “we” or “us”) understands how | ||
| 12 | important privacy is to our community. We wrote this Privacy Policy to explain what Personal Information | ||
| 13 | (“Information”) we collect through our offline editor (the “<a | ||
| 14 | href="https://wiki.openblock.cc/zh/download-software/" | ||
| 15 | target="_blank" | ||
| 16 | rel="noopener noreferrer" | ||
| 17 | >OpenBlock App</a>”), how we use, process, and share it, and what we're doing to keep it safe. It | ||
| 18 | also tells you about your rights and choices with respect to your Personal Information, and how you can <a | ||
| 19 | href="https://www.openblock.cc/contact-us/" | ||
| 20 | target="_blank" | ||
| 21 | rel="noopener noreferrer" | ||
| 22 | >contact us</a> if you have any questions or concerns. | ||
| 23 | </p> | ||
| 24 | <h2>What Information Does OpenBlock Collect About Me?</h2> | ||
| 25 | <p> | ||
| 26 | For the purpose of this Privacy Policy, “Information” means any information relating to an | ||
| 27 | identified or identifiable individual. The OpenBlock App automatically collects and stores locally the | ||
| 28 | following Information through its telemetry system: the title of your project in text form, language | ||
| 29 | setting, time zone and events related to your use of the OpenBlock App (namely when the OpenBlock App was | ||
| 30 | opened and closed, if a project file has been loaded or saved, or if a new project is created). If you | ||
| 31 | choose to turn on the telemetry sharing feature, the OpenBlock App will transmit this information to OpenBlock. | ||
| 32 | Projects created in the OpenBlock App are not transmitted to or accessible by OpenBlock. | ||
| 33 | </p> | ||
| 34 | <h2>How Does OpenBlock Use My Information?</h2> | ||
| 35 | <p>We use this Information for the following purposes:</p> | ||
| 36 | <ul> | ||
| 37 | <li> | ||
| 38 | <b>Analytics and Improving the OpenBlock App</b> - We use the Information to analyze use of the OpenBlock | ||
| 39 | App and to enhance your learning experience on the OpenBlock App. | ||
| 40 | </li> | ||
| 41 | <li> | ||
| 42 | <b>Legal</b> - We may use your Information to enforce our <a | ||
| 43 | href="https://www.openblock.cc/terms-of-use" | ||
| 44 | target="_blank" | ||
| 45 | rel="noopener noreferrer" | ||
| 46 | >Terms of Use</a>, to defend our legal rights, and to comply with our legal obligations and internal | ||
| 47 | policies. We may do this by analyzing your use of the OpenBlock App. | ||
| 48 | </li> | ||
| 49 | </ul> | ||
| 50 | <h2>What Are The Legal Grounds For Processing Your Information?</h2> | ||
| 51 | <p> | ||
| 52 | If you are located in the European Economic Area, the United Kingdom or Switzerland, we only process your | ||
| 53 | Information based on a valid legal ground. A “legal ground” is a reason that justifies our use | ||
| 54 | of your Information. In this case, we or a third party have a legitimate interest in using your Information | ||
| 55 | (if you choose to allow the OpenBlock App to send the OpenBlock team your Information) to create, analyze and | ||
| 56 | share your aggregated or de-identified Information for research purposes, to analyze and enhance your | ||
| 57 | learning experience on the OpenBlock App and otherwise ensure and improve the safety, security, and | ||
| 58 | performance of the OpenBlock App. We only rely on our or a third party’s legitimate interests to process your | ||
| 59 | Information when these interests are not overridden by your rights and interests. | ||
| 60 | </p> | ||
| 61 | <h2>How Does OpenBlock Share My Information?</h2> | ||
| 62 | <p> | ||
| 63 | We disclose information that we collect through the OpenBlock App to third parties in the following | ||
| 64 | circumstances: | ||
| 65 | </p> | ||
| 66 | <ul> | ||
| 67 | <li> | ||
| 68 | <b>Service Providers</b> - To third parties who provide services such as website hosting, data | ||
| 69 | analysis, Information technology and related infrastructure provisions, customer service, email | ||
| 70 | delivery, and other services. | ||
| 71 | </li> | ||
| 72 | <li> | ||
| 73 | <b>Merger</b> - To a potential or actual acquirer, successor, or assignee as part of any | ||
| 74 | reorganization, merger, sale, joint venture, assignment, transfer, or other disposition of all or any | ||
| 75 | portion of our organization or assets. You will have the opportunity to opt out of any such transfer if | ||
| 76 | the new entity's planned processing of your Information differs materially from that set forth in | ||
| 77 | this Privacy Policy. | ||
| 78 | </li> | ||
| 79 | <li> | ||
| 80 | <b>Legal</b> - If required to do so by law or in the good faith belief that such action is appropriate: | ||
| 81 | (a) under applicable law, including laws outside your country of residence; (b) to comply with legal | ||
| 82 | process; (c) to respond to requests from public and government authorities, such as school, school | ||
| 83 | districts, and law enforcement, including public and government authorities outside your country of | ||
| 84 | residence; (d) to enforce our terms and conditions; (e) to protect our operations or those of any of | ||
| 85 | our affiliates; (f) to protect our rights, privacy, safety, or property, and/or that of our affiliates, | ||
| 86 | you, or others; and (g) to allow us to pursue available remedies or limit the damages that we may | ||
| 87 | sustain. | ||
| 88 | </li> | ||
| 89 | </ul> | ||
| 90 | <h2>Children and Student Privacy</h2> | ||
| 91 | <p> | ||
| 92 | The OpenBlock Team is a nonprofit organization. As such, the Children's Online Privacy | ||
| 93 | Protection Act (COPPA) does not apply to OpenBlock. Nevertheless, OpenBlock takes children's privacy | ||
| 94 | seriously. OpenBlock collects only minimal information from its users, and only uses and discloses | ||
| 95 | information to provide the services and for limited other purposes, such as research, as described in this | ||
| 96 | Privacy Policy. | ||
| 97 | </p> | ||
| 98 | <p> | ||
| 99 | OpenBlock does not collect information from a student's education record, as defined by the Family | ||
| 100 | Educational Rights and Privacy Act (FERPA). OpenBlock does not disclose information of students to any third | ||
| 101 | parties except as described in this Privacy Policy. | ||
| 102 | </p> | ||
| 103 | <h2>Your Data Protection Rights (EEA)</h2> | ||
| 104 | <p> | ||
| 105 | If you are located in the European Economic Area, the United Kingdom or Switzerland, you have certain | ||
| 106 | rights in relation to your Information: | ||
| 107 | </p> | ||
| 108 | <ul> | ||
| 109 | <li> | ||
| 110 | <b>Access, Correction and Data Portability</b> - You may ask for an overview of the Information we | ||
| 111 | process about you and to receive a copy of your Information. You also have the right to request to | ||
| 112 | correct incomplete, inaccurate or outdated Information. To the extent required by applicable law, you | ||
| 113 | may request us to provide your Information to another company. | ||
| 114 | </li> | ||
| 115 | <li> | ||
| 116 | <b>Objection</b> – You may object to (this means “ask us to stop”) any use of your | ||
| 117 | Information that is not (i) processed to comply with a legal obligation, (ii) necessary to do what is | ||
| 118 | provided in a contract between OpenBlock and you, or (iii) if we have a compelling reason to do so (such | ||
| 119 | as, to ensure safety and security in our online community). If you do object, we will work with you to | ||
| 120 | find a reasonable solution. | ||
| 121 | </li> | ||
| 122 | <li> | ||
| 123 | <b>Deletion</b> - You may also request the deletion of your Information, as permitted under applicable | ||
| 124 | law. This applies, for instance, where your Information is outdated or the processing is not necessary | ||
| 125 | or is unlawful; where you withdraw your consent to our processing based on such consent; or where you | ||
| 126 | have objected to our processing. In some situations, we may need to retain your Information due to | ||
| 127 | legal obligations or for litigation purposes. If you want to have all of your Information removed from | ||
| 128 | our servers, please contact <a | ||
| 129 | href="mailto:help@openblock.cc" | ||
| 130 | target="_blank" | ||
| 131 | rel="noopener noreferrer" | ||
| 132 | >help@openblock.cc</a> for assistance. | ||
| 133 | </li> | ||
| 134 | <li> | ||
| 135 | <b>Restriction Of Processing</b> - You may request that we restrict processing of your Information | ||
| 136 | while we are processing a request relating to (i) the accuracy of your Information, (ii) the lawfulness | ||
| 137 | of the processing of your Information, or (iii) our legitimate interests to process this Information. | ||
| 138 | You may also request that we restrict processing of your Information if you wish to use the Information | ||
| 139 | for litigation purposes. | ||
| 140 | </li> | ||
| 141 | <li> | ||
| 142 | <b>Withdrawal Of Consent</b> – Where we rely on consent for the processing of your Information, you | ||
| 143 | have the right to withdraw it at any time and free of charge. When you do so, this will not affect the | ||
| 144 | lawfulness of the processing before your consent withdrawal. | ||
| 145 | </li> | ||
| 146 | </ul> | ||
| 147 | <p> | ||
| 148 | In addition to the above-mentioned rights, you also have the right to lodge a complaint with a competent | ||
| 149 | supervisory authority subject to applicable law. However, there are exceptions and limitations to each of | ||
| 150 | these rights. We may, for example, refuse to act on a request if the request is manifestly unfounded or | ||
| 151 | excessive, or if the request is likely to adversely affect the rights and freedoms of others, prejudice the | ||
| 152 | execution or enforcement of the law, interfere with pending or future litigation, or infringe applicable | ||
| 153 | law. To submit a request to exercise your rights, please contact <a | ||
| 154 | href="mailto:help@openblock.cc" | ||
| 155 | target="_blank" | ||
| 156 | rel="noopener noreferrer" | ||
| 157 | >help@openblock.cc</a> for assistance. | ||
| 158 | </p> | ||
| 159 | <h2>Data Retention</h2> | ||
| 160 | <p> | ||
| 161 | We take measures to delete your Information or keep it in a form that does not allow you to be identified | ||
| 162 | when this Information is no longer necessary for the purposes for which we process it, unless we are | ||
| 163 | required by law to keep this Information for a longer period. When determining the retention period, we | ||
| 164 | take into account various criteria, such as the type of services requested by or provided to you, the | ||
| 165 | nature and length of our relationship with you, possible re-enrollment with our services, the impact on the | ||
| 166 | services we provide to you if we delete some Information from or about you, mandatory retention periods | ||
| 167 | provided by law and the statute of limitations. | ||
| 168 | </p> | ||
| 169 | <h2>How Does OpenBlock Protect My Information?</h2> | ||
| 170 | <p> | ||
| 171 | OpenBlock has in place administrative, physical, and technical procedures that are intended to protect the | ||
| 172 | Information we collect on the OpenBlock App against accidental or unlawful destruction, accidental loss, | ||
| 173 | unauthorized alteration, unauthorized disclosure or access, misuse, and any other unlawful form of | ||
| 174 | processing of the Information. However, as effective as these measures are, no security system is | ||
| 175 | impenetrable. We cannot completely guarantee the security of our databases, nor can we guarantee that the | ||
| 176 | Information you supply will not be intercepted while being transmitted to us over the Internet. | ||
| 177 | </p> | ||
| 178 | <h2>International Data Transfer</h2> | ||
| 179 | <p> | ||
| 180 | We may transfer your Information to countries other than the country where you are located, including to | ||
| 181 | the China. (where our OpenBlock servers are located) or any other country in which we or our service | ||
| 182 | providers maintain facilities. If you are located in the European Economic Area, the United Kingdom or | ||
| 183 | Switzerland, or other regions with laws governing data collection and use that may differ from U.S. law, | ||
| 184 | please note that we may transfer your Information to a country and jurisdiction that does not have the same | ||
| 185 | data protection laws as your jurisdiction. We apply appropriate safeguards to the Information processed and | ||
| 186 | transferred on our behalf. Please contact us for more information on the safeguards used. | ||
| 187 | </p> | ||
| 188 | <h2>Notifications Of Changes To The Privacy Policy</h2> | ||
| 189 | <p> | ||
| 190 | We review our Privacy Policy on a periodic basis, and we may modify our policies as appropriate. We will | ||
| 191 | notify you of any material changes. We encourage you to review our Privacy Policy on a regular basis. The | ||
| 192 | “Last Updated” date at the top of this page indicates when this Privacy Policy was last | ||
| 193 | revised. Your continued use of the OpenBlock App following these changes means that you accept the revised | ||
| 194 | Privacy Policy. | ||
| 195 | </p> | ||
| 196 | <h2>Contact Us</h2> | ||
| 197 | <p> | ||
| 198 | The OpenBlock Team is the entity responsible for the processing of your Information. If you have any | ||
| 199 | questions about this Privacy Policy, or if you would like to exercise your rights to your Information, you | ||
| 200 | may contact us at <a | ||
| 201 | href="mailto:help@openblock.cc" | ||
| 202 | target="_blank" | ||
| 203 | rel="noopener noreferrer" | ||
| 204 | >help@openblock.cc</a> or via mail at: | ||
| 205 | </p> | ||
| 206 | <div className="vcard"> | ||
| 207 | <div className="org">OpenBlock Team</div> | ||
| 208 | <div className="fn">ATTN: Privacy Policy</div> | ||
| 209 | <div className="adr"> | ||
| 210 | <div className="street-address">Datang Street</div> | ||
| 211 | <span className="locality">Shenzhen</span>, <span className="region">Guangdong</span> <span | ||
| 212 | className="postal-code" | ||
| 213 | >518110</span> | ||
| 214 | </div> | ||
| 215 | </div> | ||
| 216 | </div> | ||
| 217 | ); | ||
| 218 | |||
| 219 | export default <PrivacyElement />; |
src/renderer/showPrivacyPolicy.js
0 → 100644
| 1 | import {ipcRenderer} from 'electron'; | ||
| 2 | |||
| 3 | const showPrivacyPolicy = event => { | ||
| 4 | if (event) { | ||
| 5 | // Probably a click on a link; don't actually follow the link in the `href` attribute. | ||
| 6 | event.preventDefault(); | ||
| 7 | } | ||
| 8 | // tell the main process to open the privacy policy window | ||
| 9 | ipcRenderer.send('open-privacy-policy-window'); | ||
| 10 | return false; | ||
| 11 | }; | ||
| 12 | |||
| 13 | export default showPrivacyPolicy; |
webpack.main.js
0 → 100644
| 1 | const path = require('path'); | ||
| 2 | |||
| 3 | const makeConfig = require('./webpack.makeConfig.js'); | ||
| 4 | |||
| 5 | module.exports = defaultConfig => | ||
| 6 | makeConfig( | ||
| 7 | defaultConfig, | ||
| 8 | { | ||
| 9 | name: 'main', | ||
| 10 | useReact: false, | ||
| 11 | disableDefaultRulesForExtensions: ['js'], | ||
| 12 | babelPaths: [ | ||
| 13 | path.resolve(__dirname, 'src', 'main') | ||
| 14 | ] | ||
| 15 | } | ||
| 16 | ); |
webpack.makeConfig.js
0 → 100644
| 1 | const childProcess = require('child_process'); | ||
| 2 | const fs = require('fs'); | ||
| 3 | const path = require('path'); | ||
| 4 | const util = require('util'); | ||
| 5 | |||
| 6 | const electronPath = require('electron'); | ||
| 7 | const webpack = require('webpack'); | ||
| 8 | const merge = require('webpack-merge'); | ||
| 9 | |||
| 10 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); | ||
| 11 | const MONACO_DIR = path.resolve(__dirname, './node_modules/monaco-editor'); | ||
| 12 | |||
| 13 | // PostCss | ||
| 14 | const autoprefixer = require('autoprefixer'); | ||
| 15 | const postcssVars = require('postcss-simple-vars'); | ||
| 16 | const postcssImport = require('postcss-import'); | ||
| 17 | |||
| 18 | const isProduction = (process.env.NODE_ENV === 'production'); | ||
| 19 | |||
| 20 | const electronVersion = childProcess.execSync(`${electronPath} --version`, {encoding: 'utf8'}).trim(); | ||
| 21 | console.log(`Targeting Electron ${electronVersion}`); // eslint-disable-line no-console | ||
| 22 | |||
| 23 | const makeConfig = function (defaultConfig, options) { | ||
| 24 | const babelOptions = { | ||
| 25 | // Explicitly disable babelrc so we don't catch various config in much lower dependencies. | ||
| 26 | babelrc: false, | ||
| 27 | plugins: [ | ||
| 28 | '@babel/plugin-syntax-dynamic-import', | ||
| 29 | '@babel/plugin-transform-async-to-generator', | ||
| 30 | '@babel/plugin-proposal-object-rest-spread' | ||
| 31 | ], | ||
| 32 | presets: [ | ||
| 33 | ['@babel/preset-env', {targets: {electron: electronVersion}}] | ||
| 34 | ] | ||
| 35 | }; | ||
| 36 | |||
| 37 | const sourceFileTest = options.useReact ? /\.jsx?$/ : /\.js$/; | ||
| 38 | if (options.useReact) { | ||
| 39 | babelOptions.presets = babelOptions.presets.concat('@babel/preset-react'); | ||
| 40 | babelOptions.plugins.push(['react-intl', { | ||
| 41 | messagesDir: './translations/messages/' | ||
| 42 | }]); | ||
| 43 | } | ||
| 44 | |||
| 45 | // TODO: consider adjusting these rules instead of discarding them in at least some cases | ||
| 46 | if (options.disableDefaultRulesForExtensions) { | ||
| 47 | defaultConfig.module.rules = defaultConfig.module.rules.filter(rule => { | ||
| 48 | if (!(rule.test instanceof RegExp)) { | ||
| 49 | // currently we don't support overriding other kinds of rules | ||
| 50 | return true; | ||
| 51 | } | ||
| 52 | // disable default rules for any file extension listed here | ||
| 53 | // we will handle these files in some other way (see below) | ||
| 54 | // OR we want to avoid any processing at all (such as with fonts) | ||
| 55 | const shouldDisable = options.disableDefaultRulesForExtensions.some( | ||
| 56 | ext => rule.test.test(`test.${ext}`) | ||
| 57 | ); | ||
| 58 | const statusWord = shouldDisable ? 'Discarding' : 'Keeping'; | ||
| 59 | console.log(`${options.name}: ${statusWord} electron-webpack default rule for ${rule.test}`); | ||
| 60 | return !shouldDisable; | ||
| 61 | }); | ||
| 62 | } | ||
| 63 | |||
| 64 | const config = merge.smart(defaultConfig, { | ||
| 65 | devtool: 'cheap-module-eval-source-map', | ||
| 66 | mode: isProduction ? 'production' : 'development', | ||
| 67 | module: { | ||
| 68 | rules: [ | ||
| 69 | { | ||
| 70 | test: sourceFileTest, | ||
| 71 | include: options.babelPaths, | ||
| 72 | loader: 'babel-loader', | ||
| 73 | options: babelOptions | ||
| 74 | }, | ||
| 75 | { // coped from scratch-gui | ||
| 76 | test: /\.css$/, | ||
| 77 | exclude: MONACO_DIR, | ||
| 78 | use: [{ | ||
| 79 | loader: 'style-loader' | ||
| 80 | }, { | ||
| 81 | loader: 'css-loader', | ||
| 82 | options: { | ||
| 83 | modules: true, | ||
| 84 | importLoaders: 1, | ||
| 85 | localIdentName: '[name]_[local]_[hash:base64:5]', | ||
| 86 | camelCase: true | ||
| 87 | } | ||
| 88 | }, { | ||
| 89 | loader: 'postcss-loader', | ||
| 90 | options: { | ||
| 91 | ident: 'postcss', | ||
| 92 | plugins: function () { | ||
| 93 | return [ | ||
| 94 | postcssImport, | ||
| 95 | postcssVars, | ||
| 96 | autoprefixer | ||
| 97 | ]; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | }] | ||
| 101 | }, | ||
| 102 | { | ||
| 103 | test: /\.(svg|png|wav|gif|jpg|ttf)$/, | ||
| 104 | loader: 'file-loader', | ||
| 105 | options: { | ||
| 106 | outputPath: 'static/assets/' | ||
| 107 | } | ||
| 108 | }, | ||
| 109 | { | ||
| 110 | test: /\.css$/, | ||
| 111 | include: MONACO_DIR, | ||
| 112 | use: ['style-loader', 'css-loader'] | ||
| 113 | }, | ||
| 114 | { | ||
| 115 | test: /node_modules[/\\](iconv-lite)[/\\].+/, | ||
| 116 | resolve: { | ||
| 117 | aliasFields: ['main'] | ||
| 118 | } | ||
| 119 | } | ||
| 120 | ] | ||
| 121 | }, | ||
| 122 | plugins: [ | ||
| 123 | new webpack.DefinePlugin({ | ||
| 124 | 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"` | ||
| 125 | }), | ||
| 126 | new webpack.SourceMapDevToolPlugin({ | ||
| 127 | filename: '[file].map' | ||
| 128 | }), | ||
| 129 | new MonacoWebpackPlugin({ | ||
| 130 | languages: ['c', 'cpp', 'python', 'lua', 'javascript'], | ||
| 131 | features: ['!gotoSymbol'] | ||
| 132 | }) | ||
| 133 | ].concat(options.plugins || []), | ||
| 134 | resolve: { | ||
| 135 | cacheWithContext: false, | ||
| 136 | symlinks: false, | ||
| 137 | alias: { | ||
| 138 | // act like scratch-gui has this line in its package.json: | ||
| 139 | // "browser": "./src/index.js" | ||
| 140 | 'openblock-gui$': path.resolve(__dirname, 'node_modules', 'openblock-gui', 'src', 'index.js') | ||
| 141 | } | ||
| 142 | } | ||
| 143 | }); | ||
| 144 | |||
| 145 | // If we're not on CI, enable Webpack progress output | ||
| 146 | // Note that electron-webpack enables this by default, so use '--no-progress' to avoid double-adding this plugin | ||
| 147 | if (!process.env.CI) { | ||
| 148 | config.plugins.push(new webpack.ProgressPlugin()); | ||
| 149 | } | ||
| 150 | |||
| 151 | fs.writeFileSync( | ||
| 152 | `dist/webpack.${options.name}.js`, | ||
| 153 | `module.exports = ${util.inspect(config, {depth: null})};\n` | ||
| 154 | ); | ||
| 155 | |||
| 156 | return config; | ||
| 157 | }; | ||
| 158 | |||
| 159 | module.exports = makeConfig; |
webpack.renderer.js
0 → 100644
| 1 | const path = require('path'); | ||
| 2 | |||
| 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); | ||
| 4 | |||
| 5 | const makeConfig = require('./webpack.makeConfig.js'); | ||
| 6 | |||
| 7 | const getModulePath = moduleName => path.dirname(require.resolve(`${moduleName}/package.json`)); | ||
| 8 | |||
| 9 | module.exports = defaultConfig => | ||
| 10 | makeConfig( | ||
| 11 | defaultConfig, | ||
| 12 | { | ||
| 13 | name: 'renderer', | ||
| 14 | useReact: true, | ||
| 15 | disableDefaultRulesForExtensions: ['js', 'jsx', 'css', 'svg', 'png', 'wav', 'gif', 'jpg', 'ttf'], | ||
| 16 | babelPaths: [ | ||
| 17 | path.resolve(__dirname, 'src', 'renderer'), | ||
| 18 | /node_modules[\\/]+scratch-[^\\/]+[\\/]+src/, | ||
| 19 | /node_modules[\\/]+openblock-[^\\/]+[\\/]+src/, | ||
| 20 | /node_modules[\\/]+pify/, | ||
| 21 | /node_modules[\\/]+@vernier[\\/]+godirect/ | ||
| 22 | ], | ||
| 23 | plugins: [ | ||
| 24 | new CopyWebpackPlugin([{ | ||
| 25 | from: path.join(getModulePath('openblock-blocks'), 'media'), | ||
| 26 | to: 'static/blocks-media' | ||
| 27 | }]), | ||
| 28 | new CopyWebpackPlugin([{ | ||
| 29 | from: 'extension-worker.{js,js.map}', | ||
| 30 | context: path.join(getModulePath('openblock-vm'), 'dist', 'web') | ||
| 31 | }]), | ||
| 32 | new CopyWebpackPlugin([{ | ||
| 33 | from: path.join(getModulePath('openblock-gui'), 'src', 'lib', 'libraries', '*.json'), | ||
| 34 | to: 'static/libraries', | ||
| 35 | flatten: true | ||
| 36 | }]) | ||
| 37 | ] | ||
| 38 | } | ||
| 39 | ); |
-
Please register or sign in to post a comment