a6e4ddd9 by cx19940809

first-commit

0 parents
Showing 83 changed files with 5081 additions and 0 deletions
1 root = true
2
3 [*]
4 end_of_line = lf
5 insert_final_newline = true
6 charset = utf-8
7 indent_size = 4
8 trim_trailing_whitespace = true
9
10 [*.{js,html}]
11 indent_style = space
1 node_modules/*
2 dist/*
1 module.exports = {
2 root: true,
3 env: {
4 node: true
5 },
6 extends: ['scratch', 'scratch/es6', 'scratch/node']
7 };
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
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 ![Block sketch](https://user-images.githubusercontent.com/3431616/77192550-1dcebe00-6ab3-11ea-9606-8ecd8500c958.png)
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
1 ko_fi: arthurzheng
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_
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_
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
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
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
1 # Mac OS
2 .DS_Store
3
4 # NPM
5 /node_modules
6 npm-*
7
8 # Testing
9 /.nyc_output
10 /coverage
11
12 # Build
13 /.opt-in
14
15 # Temporary resource
16 /external-resources
17 /tools
18 /firmwares
19 /drivers
20
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 }
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
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
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.
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 ![screenshot](./doc/screenshot.png)
21 ![screenshot2](./doc/screenshot2.png)
22
23 ## Getting Start
24
25 Visit the wiki: [https://openblockcc.github.io](https://openblockcc.github.io)
26
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.
1 /ScratchDesktop.iconset
2 /tmp
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
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>
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>
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
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
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
1 {
2 "main": {
3 "webpackConfig": "webpack.main.js"
4 },
5 "renderer": {
6 "template": "src/renderer/index.html",
7 "webpackConfig": "webpack.renderer.js"
8 }
9 }
This diff could not be displayed because it is too large.
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 }
1 module.exports = {
2 rules: {
3 'no-console': 'off'
4 }
5 };
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;
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 });
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();
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();
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;
1 module.exports = {
2 root: true,
3 env: {
4 node: true
5 },
6 extends: ['scratch', 'scratch/es6'],
7 globals: {
8 __static: false // electron-webpack provides this constant to access bundled static assets
9 }
10 };
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;
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}`);
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
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
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>
1 module.exports = {
2 root: true,
3 env: {
4 node: true
5 },
6 extends: ['scratch', 'scratch/es6', 'scratch/node']
7 };
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 };
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 ]);
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;
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;
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;
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;
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);
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;
1 module.exports = {
2 root: true,
3 env: {
4 browser: true,
5 node: true
6 },
7 extends: ['scratch', 'scratch/es6', 'scratch/react'],
8 settings: {
9 react: {
10 version: '16.2' // Prevent 16.3 lifecycle method errors
11 }
12 }
13 };
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;
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;
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 }
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 />;
1 /* Adapted from scratch-gui/src/playground/index.css */
2 html,
3 body,
4 .app {
5 width: 100%;
6 height: 100%;
7 margin: 0;
8
9 /* Setting min height/width makes the UI scroll below those sizes */
10 min-width: 1024px;
11 min-height: 640px; /* Min height to fit sprite/backdrop button */
12 }
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 />;
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>
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 });
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 }
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 &quot;Software&quot;), 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 &quot;AS IS&quot;, 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 &quot;AS IS&quot; 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 &copy; {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 />;
1 html, body {
2 background-color: transparent;
3 }
4
5 .loadingBox {
6 margin: 0;
7 position: absolute;
8 top: 50%;
9 left: 50%;
10 transform: translate(-50%, -50%);
11 width: 100%;
12 height: 100%;
13 }
14
15 .loadingLogo {
16 max-width: 50rem;
17 }
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 />;
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 .privacyBox {
10 background-color: white;
11 color: #575e75;
12 margin: 3rem;
13 padding: 2rem 3rem;
14 }
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 (&ldquo;OpenBlock&rdquo;, &ldquo;we&rdquo; or &ldquo;us&rdquo;) understands how
12 important privacy is to our community. We wrote this Privacy Policy to explain what Personal Information
13 (&ldquo;Information&rdquo;) we collect through our offline editor (the &ldquo;<a
14 href="https://wiki.openblock.cc/zh/download-software/"
15 target="_blank"
16 rel="noopener noreferrer"
17 >OpenBlock App</a>&rdquo;), how we use, process, and share it, and what we&apos;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, &ldquo;Information&rdquo; 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 &ldquo;legal ground&rdquo; 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&apos;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&apos;s Online Privacy
93 Protection Act (COPPA) does not apply to OpenBlock. Nevertheless, OpenBlock takes children&apos;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&apos;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 &ldquo;ask us to stop&rdquo;) 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 &ldquo;Last Updated&rdquo; 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 />;
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;
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 );
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;
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 );
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!