├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── LICENSE
├── README-zh_CN.md
├── README.md
├── favicon.ico
├── index.html
├── package.json
├── plopfile.js
├── plugin-template
├── plugin-index-js.hbs
├── plugin-index-react-js.hbs
├── plugin-index-react-ts.hbs
├── plugin-index-ts.hbs
├── plugin-manifest.hbs
└── styles.hbs
├── pnpm-lock.yaml
├── postcss.config.js
├── scripts
├── build-single-plugin.js
├── func-plugin-wrapper.hbs
└── rc-plugin-wrapper.hbs
├── src
├── assets
│ ├── icon--BetterSpriteMenu.svg
│ ├── icon--batch-select.svg
│ ├── icon--block-sharing.svg
│ ├── icon--clean-pro.svg
│ ├── icon--clones.svg
│ ├── icon--code-filter.svg
│ ├── icon--code-find.svg
│ ├── icon--costumepiskel.svg
│ ├── icon--custom-css.svg
│ ├── icon--custom-plugin.svg
│ ├── icon--data-category-tweaks.svg
│ ├── icon--dev-tools.svg
│ ├── icon--down.svg
│ ├── icon--draggable.svg
│ ├── icon--dropdown-searchable.svg
│ ├── icon--extension-manager.svg
│ ├── icon--fast-input.svg
│ ├── icon--inspiro-import.svg
│ ├── icon--inspiro.svg
│ ├── icon--kukemcbeautify.svg
│ ├── icon--list.svg
│ ├── icon--multiselect-box.svg
│ ├── icon--plugins-manage.svg
│ ├── icon--statistics.svg
│ ├── icon--switchCode.svg
│ ├── icon--tack.svg
│ ├── icon--tacked.svg
│ ├── icon--terminal.svg
│ ├── icon--trashcan.svg
│ ├── icon--variables.svg
│ ├── icon--voice--downarrow.svg
│ ├── icon--voice--expand.svg
│ ├── icon--voice--microphone.svg
│ ├── icon--voice--muted-microphone.svg
│ ├── icon--voice--off-white.svg
│ ├── icon--voice--off.svg
│ ├── icon--voice--setting.svg
│ ├── icon--voice--uparrow.svg
│ ├── icon--voice.svg
│ └── icon--witcat-blockinput.svg
├── components
│ ├── Bubble
│ │ ├── index.tsx
│ │ └── styles.less
│ ├── ExpansionBox
│ │ ├── index.tsx
│ │ └── styles.less
│ ├── IF
│ │ └── index.tsx
│ ├── Tab
│ │ ├── index.tsx
│ │ └── styles.less
│ └── Tooltip
│ │ ├── index.tsx
│ │ └── styles.less
├── hooks
│ └── useStorageInfo.ts
├── index.tsx
├── l10n
│ ├── en.json
│ ├── es.json
│ ├── ms.json
│ ├── ru.json
│ ├── uk.json
│ └── zh-cn.json
├── lib
│ ├── block-media.ts
│ ├── client-info.ts
│ └── code-hash.json
├── main.ts
├── plugins-controller.ts
├── plugins-entry.ts
├── plugins-l10n.ts
├── plugins-manifest.ts
├── plugins
│ ├── better-sprite-menu
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── block-sharing
│ │ ├── components
│ │ │ ├── Article.tsx
│ │ │ ├── ArticleList.tsx
│ │ │ ├── BluePrint.tsx
│ │ │ ├── BluePrintList.tsx
│ │ │ ├── Demo.tsx
│ │ │ ├── DemoList.tsx
│ │ │ └── Home.tsx
│ │ ├── hack.js
│ │ ├── icons
│ │ │ └── home.svg
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ ├── material-drag.ts
│ │ └── styles.less
│ ├── clean-pro
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── code-batch-select
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ ├── styles.less
│ │ ├── useBatchSelect.ts
│ │ ├── useCheeryPick.ts
│ │ ├── useKeyDownOperate.ts
│ │ └── useRightContextMenu.ts
│ ├── code-filter
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── code-find
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── code-switch
│ │ ├── const.ts
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── costume-piskel
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── custom-css
│ │ ├── PresetThemesList.md
│ │ ├── colorUtils.ts
│ │ ├── index.ts
│ │ ├── manifest.ts
│ │ └── presetThemes.less
│ ├── custom-plugin
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── data-category-tweaks
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── dev-tools
│ │ ├── components
│ │ │ ├── CollapsibleItemView
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ ├── Content
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ ├── Entrance
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ ├── ListView
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ ├── TackedVariableView
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ ├── TackedVariables
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ ├── TargetView
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ ├── VariableView
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ │ └── VariablesView
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.less
│ │ ├── index.tsx
│ │ ├── lib
│ │ │ ├── context.ts
│ │ │ ├── dev-tools-observer.ts
│ │ │ ├── event-bus.ts
│ │ │ └── proxy-variable.ts
│ │ └── manifest.ts
│ ├── dropdown-searchable
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── extension-manager
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── fast-input
│ │ ├── BlockRenderer.js
│ │ ├── BlockTypeInfo.js
│ │ ├── VirtualScroller.ts
│ │ ├── WorkspaceQuerier.js
│ │ ├── compatibility.css
│ │ ├── index.ts
│ │ ├── manifest.ts
│ │ ├── module.js
│ │ ├── pinyin_dict_notone.js
│ │ └── styles.css
│ ├── folder
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── historical-version
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── inspiro
│ │ ├── componet
│ │ │ ├── Chat.tsx
│ │ │ ├── Contact.ts
│ │ │ └── Entrance.tsx
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── kukemc-beautify
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── mobile-code-batch-select
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ ├── styles.less
│ │ ├── touchZoom.ts
│ │ ├── useBatchSelect.ts
│ │ ├── useCheeryPick.ts
│ │ ├── useKeyDownOperate.ts
│ │ └── useRightContextMenu.ts
│ ├── plugins-manager
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── statistics
│ │ ├── index.tsx
│ │ └── manifest.ts
│ ├── terminal
│ │ ├── index.tsx
│ │ ├── manifest.ts
│ │ └── styles.less
│ ├── voice-cooperation
│ │ ├── components
│ │ │ ├── MemberList
│ │ │ │ ├── MemberListItem.less
│ │ │ │ └── MemberListItem.tsx
│ │ │ ├── VoiceFloating
│ │ │ │ ├── VoiceFloating.less
│ │ │ │ └── VoiceFloating.tsx
│ │ │ └── VoiceFloatingNew
│ │ │ │ ├── VoiceFloatingNew.less
│ │ │ │ └── VoiceFloatingNew.tsx
│ │ ├── config.ts
│ │ ├── dots.less
│ │ ├── index.tsx
│ │ ├── lib
│ │ │ └── livekit.ts
│ │ ├── manifest.ts
│ │ └── styles.less
│ └── witcat-blockinput
│ │ ├── index.ts
│ │ ├── lineText.js
│ │ ├── manifest.ts
│ │ └── style.less
├── types.d.ts
├── types
│ ├── blockly.d.ts
│ ├── interface.d.ts
│ ├── scratch.d.ts
│ ├── teamwork.d.ts
│ └── utils.d.ts
└── utils
│ ├── block-flasher.ts
│ ├── block-helper.ts
│ ├── blocks-keywords-parser.ts
│ ├── color.ts
│ ├── dom-helper.ts
│ ├── hotkey-helper.ts
│ ├── index.ts
│ ├── name-helper.ts
│ ├── workspace-utils.ts
│ └── xml.ts
├── tsconfig.json
├── webpack.config.js
└── webpackDevServer.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | tab_width = 2
11 | insert_final_newline = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
16 | [Makefile]
17 | indent_style = tab
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | build/*
3 | dist/*
4 | temp-wrapper.jsx
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es2021": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended"],
9 | "overrides": [],
10 | "parser": "@typescript-eslint/parser",
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": ["react", "@typescript-eslint", "prettier"],
16 | "rules": {
17 | "prettier/prettier": "error",
18 | "brace-style": "error",
19 | "no-tabs": 0,
20 | "space-before-function-paren": 0,
21 | "eol-last": 0,
22 | "no-unused-expressions": 0,
23 | "@typescript-eslint/no-var-requires": "off",
24 | "arrow-body-style": "off",
25 | "prefer-arrow-callback": "off",
26 | "react/display-name": "off"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .vscode
4 |
5 | # dependencies
6 | /node_modules
7 |
8 | # production
9 | /dist
10 | /lib
11 |
12 | # log
13 | *.log
14 |
15 | # style types
16 | *.less.d.ts
17 |
18 | temp-wrapper.jsx
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "tabWidth": 2,
4 | "printWidth": 120,
5 | "trailingComma": "all",
6 | "bracketSpacing": true,
7 | "semi": true
8 | }
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gandi-IDE/gandi-plugins/ed67c6596967de3fa7e08bc3d96687f330b67c8b/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gandi-plugins",
3 | "version": "0.0.1",
4 | "description": "Plug-ins for Gandi-IDE.",
5 | "main": "dist/index.js",
6 | "types": "lib/main.d.ts",
7 | "author": "Luka (https://github.com/zxq142857)",
8 | "license": "LGPL-3.0-only",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/Gandi-IDE/gandi-plugins.git"
12 | },
13 | "scripts": {
14 | "createPlugin": "plop",
15 | "start": "webpack-dev-server --config webpackDevServer.config.js",
16 | "startHostCocrea": "cross-env SITE=COCREA webpack-dev-server --config webpackDevServer.config.js",
17 | "build": "webpack --config webpack.config.js --progress",
18 | "buildSinglePlugin": "node scripts/build-single-plugin.js",
19 | "lint": "eslint src --ext .js,.ts",
20 | "prepublishOnly": "pnpm install && pnpm build",
21 | "clean": "rimraf lib && mkdirp lib && rimraf ./dist && mkdirp dist"
22 | },
23 | "files": [
24 | "dist",
25 | "lib"
26 | ],
27 | "babel": {
28 | "presets": [
29 | "@babel/preset-env"
30 | ]
31 | },
32 | "dependencies": {
33 | "@chatscope/chat-ui-kit-react": "^2.0.3",
34 | "@chatscope/chat-ui-kit-styles": "^1.4.0",
35 | "@chatscope/use-chat": "^3.1.2",
36 | "@formatjs/intl": "^2.5.1",
37 | "@gandi-ide/gandi-ui": "^1.0.8",
38 | "@legendapp/state": "^0.23.3",
39 | "@tanstack/react-virtual": "^3.0.0-beta.45",
40 | "classnames": "^2.3.2",
41 | "computed-style-to-inline-style": "^4.0.0",
42 | "deep-object-diff": "^1.1.9",
43 | "gandiblocks": "^1.0.2",
44 | "livekit-client": "^2.3.2",
45 | "lodash-es": "^4.17.21",
46 | "nanoid": "^5.0.7",
47 | "react": "^18.2.0",
48 | "react-dom": "^18.2.0",
49 | "react-draggable": "^4.4.5",
50 | "react-hot-toast": "^2.4.1",
51 | "react-intl": "^6.1.1",
52 | "react-markdown": "^10.1.0"
53 | },
54 | "devDependencies": {
55 | "@babel/core": "^7.19.3",
56 | "@babel/preset-env": "^7.19.3",
57 | "@svgr/webpack": "^6.3.1",
58 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
59 | "@types/lodash-es": "^4.17.8",
60 | "@types/node": "^20.11.30",
61 | "@types/react": "^18.0.21",
62 | "@types/react-dom": "^18.0.6",
63 | "@typescript-eslint/eslint-plugin": "^5.38.1",
64 | "@typescript-eslint/parser": "^5.38.1",
65 | "autoprefixer": "^10.4.12",
66 | "axios": "^1.7.2",
67 | "babel-loader": "^8.2.5",
68 | "copy-webpack-plugin": "10.2.4",
69 | "cors": "^2.8.5",
70 | "cross-env": "^7.0.3",
71 | "css-loader": "^6.7.1",
72 | "eslint": "^8.57.0",
73 | "eslint-config-prettier": "^9.1.0",
74 | "eslint-plugin-prettier": "^5.1.3",
75 | "eslint-plugin-react": "^7.31.8",
76 | "handlebars": "^4.7.8",
77 | "html-webpack-plugin": "^5.6.0",
78 | "less": "^4.1.3",
79 | "less-loader": "^11.0.0",
80 | "plop": "^3.1.1",
81 | "postcss": "^8.4.17",
82 | "postcss-loader": "^7.0.1",
83 | "postcss-nested": "^6.0.0",
84 | "prettier": "^3.2.5",
85 | "react": "^18.2.0",
86 | "react-dom": "^18.2.0",
87 | "rimraf": "^3.0.2",
88 | "source-map-loader": "^4.0.0",
89 | "style-loader": "^3.3.1",
90 | "ts-loader": "^9.4.1",
91 | "typescript": "^4.8.3",
92 | "webpack": "^5.90.3",
93 | "webpack-cli": "^4.10.0",
94 | "webpack-dev-server": "^4.11.1",
95 | "yargs": "^17.7.2"
96 | },
97 | "browserslist": [
98 | "last 2 versions",
99 | "> 1%"
100 | ]
101 | }
--------------------------------------------------------------------------------
/plugin-template/plugin-index-js.hbs:
--------------------------------------------------------------------------------
1 | import styles from "./styles.less";
2 |
3 | const {{ componentName }} = () => {
4 | console.log("Hello world!");
5 | return {
6 | dispose: () => {
7 | /** Remove some side effects */
8 | },
9 | };
10 | };
11 |
12 | export default {{ componentName }};
13 |
--------------------------------------------------------------------------------
/plugin-template/plugin-index-react-js.hbs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styles from "./styles.less";
3 |
4 | const {{ componentName }} = () => {
5 | return {"This is a new plugin."} ;
6 | };
7 |
8 | {{ componentName }}.displayName = "{{ componentName }}";
9 |
10 | export default {{ componentName }};
11 |
--------------------------------------------------------------------------------
/plugin-template/plugin-index-react-ts.hbs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styles from "./styles.less";
3 |
4 | const {{ componentName }}: React.FC = () => {
5 | return {"This is a new plugin."} ;
6 | };
7 |
8 | {{ componentName }}.displayName = "{{ componentName }}";
9 |
10 | export default {{ componentName }};
11 |
--------------------------------------------------------------------------------
/plugin-template/plugin-index-ts.hbs:
--------------------------------------------------------------------------------
1 | import styles from "./styles.less";
2 |
3 | const {{ componentName }} = (context: PluginContext) => {
4 | return {
5 | dispose: () => {
6 | /** Remove some side effects */
7 | },
8 | };
9 | };
10 |
11 | export default {{ componentName }};
12 |
--------------------------------------------------------------------------------
/plugin-template/plugin-manifest.hbs:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "{{ name }}",
3 | type: "{{ type }}",
4 | description: "{{ description }}",
5 | credits: [
6 | {{ credits }}
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/plugin-template/styles.hbs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gandi-IDE/gandi-plugins/ed67c6596967de3fa7e08bc3d96687f330b67c8b/plugin-template/styles.hbs
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require("autoprefixer"), require("postcss-nested")],
3 | };
4 |
--------------------------------------------------------------------------------
/scripts/func-plugin-wrapper.hbs:
--------------------------------------------------------------------------------
1 | import pluginsL10n from "src/plugins-l10n";
2 | import plugin from "./index";
3 | import { createIntl, createIntlCache } from "@formatjs/intl";
4 |
5 | const pluginName = "{{pluginName}}";
6 |
7 | window.Scratch.plugins.register((context) => {
8 | const locale = context.intl.locale;
9 | const intl = createIntl({locale, messages: pluginsL10n[locale]}, createIntlCache());
10 | const instance = plugin({
11 | ...context,
12 | intl,
13 | msg: (id) => intl.formatMessage({ id }),
14 | });
15 | return {
16 | dispose: instance.dispose || (() => {
17 | /* noop */
18 | }),
19 | };
20 | }, pluginName);
21 |
--------------------------------------------------------------------------------
/scripts/rc-plugin-wrapper.hbs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom/client";
3 | import pluginsL10n from "src/plugins-l10n";
4 | import PluginComponent from "./index";
5 | import { createIntl, createIntlCache } from "@formatjs/intl";
6 |
7 | const pluginName = "{{pluginName}}";
8 |
9 | window.Scratch.plugins.register((context) => {
10 | const div = document.createElement("div");
11 | div.setAttribute("data-plugin-name", pluginName);
12 |
13 | const pluginsWrapper = document.body.querySelector("#gandi-plugins-wrapper");
14 | pluginsWrapper.appendChild(div);
15 | const locale = context.intl.locale;
16 | const intl = createIntl({locale, messages: pluginsL10n[locale]}, createIntlCache());
17 | const Plugin = React.createElement(PluginComponent, {
18 | ...context,
19 | intl,
20 | msg: (id) => intl.formatMessage({ id }),
21 | });
22 | const root = ReactDOM.createRoot(div);
23 | root.render(Plugin);
24 |
25 | return {
26 | dispose: () => {
27 | root.unmount();
28 | pluginsWrapper.removeChild(div);
29 | },
30 | };
31 | }, pluginName);
32 |
--------------------------------------------------------------------------------
/src/assets/icon--BetterSpriteMenu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icon--batch-select.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/assets/icon--block-sharing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icon--clean-pro.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icon--clones.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
11 |
--------------------------------------------------------------------------------
/src/assets/icon--code-filter.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/icon--code-find.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
--------------------------------------------------------------------------------
/src/assets/icon--costumepiskel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icon--custom-css.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icon--custom-plugin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--data-category-tweaks.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/icon--dev-tools.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
10 |
11 |
12 |
14 |
16 |
18 |
20 |
21 |
--------------------------------------------------------------------------------
/src/assets/icon--down.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--draggable.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icon--dropdown-searchable.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--extension-manager.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/icon--fast-input.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/assets/icon--inspiro-import.svg:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/assets/icon--inspiro.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/assets/icon--kukemcbeautify.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icon--list.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icon--multiselect-box.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--plugins-manage.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/icon--statistics.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--switchCode.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--tack.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--tacked.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--terminal.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/icon--trashcan.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icon--variables.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--downarrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--expand.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--microphone.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--muted-microphone.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--off-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--setting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icon--voice--uparrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icon--voice.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icon--witcat-blockinput.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
--------------------------------------------------------------------------------
/src/components/Bubble/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import classNames from "classnames";
4 | import styles from "./styles.less";
5 |
6 | interface PopperProps {
7 | className?: string;
8 | left: number;
9 | top: number;
10 | text: string;
11 | visible: boolean;
12 | }
13 |
14 | interface BubbleProps {
15 | className?: string;
16 | title: string;
17 | children: React.ReactElement;
18 | }
19 |
20 | const Popper: React.FC = ({ className, visible, left, top, text = "" }) => {
21 | const [horizontalOffset, setHorizontalOffset] = React.useState(0);
22 | const containerRef = React.useRef();
23 |
24 | React.useEffect(() => {
25 | if (visible) {
26 | let offset = 0;
27 | const bodyWidth = document.body.offsetWidth;
28 | const width = containerRef.current.offsetWidth / 2;
29 | if (left < width) {
30 | offset = width - left;
31 | } else if (left + width > bodyWidth) {
32 | offset = bodyWidth - (left + width);
33 | }
34 | setHorizontalOffset(offset);
35 | }
36 | }, [left, visible]);
37 |
38 | return visible
39 | ? ReactDOM.createPortal(
40 | 300 ? "80vw" : "190px",
47 | display: visible ? "" : "none",
48 | }}
49 | >
50 | {text}
51 |
57 |
,
58 | document.body,
59 | )
60 | : null;
61 | };
62 |
63 | const Bubble: React.FC = (props) => {
64 | const { title = "", children } = props;
65 | const [tipVisible, setTipVisible] = React.useState(false);
66 | const position = React.useRef({ x: 0, y: 0 });
67 |
68 | const handleMouseEnter = (e: React.MouseEvent) => {
69 | const rect = (e.target as HTMLElement).getBoundingClientRect();
70 | position.current.x = rect.x + rect.width / 2;
71 | position.current.y = rect.y + rect.height + 9;
72 | setTipVisible(true);
73 | };
74 |
75 | const handleMouseLeave = () => {
76 | setTipVisible(false);
77 | };
78 |
79 | return (
80 |
81 | {React.cloneElement(children, {
82 | onMouseEnter: handleMouseEnter,
83 | onMouseLeave: handleMouseLeave,
84 | })}
85 |
86 |
87 | );
88 | };
89 |
90 | const areEqual = (prevProps: BubbleProps, nextProps: BubbleProps) =>
91 | prevProps.className === nextProps.className && prevProps.title === nextProps.title;
92 |
93 | export default React.memo(Bubble, areEqual);
94 |
--------------------------------------------------------------------------------
/src/components/Bubble/styles.less:
--------------------------------------------------------------------------------
1 | .tip {
2 | width: max-content;
3 | position: absolute;
4 | padding: 8px 12px;
5 | color: white;
6 | font-size: 12px;
7 | line-height: 18px;
8 | border-radius: 8px;
9 | box-sizing: border-box;
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 | pointer-events: none;
14 | z-index: 910;
15 | border: var(--theme-border-size-tip) solid var(--theme-border-color-tip);
16 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
17 | transform: translate(-50%, 0);
18 | }
19 |
20 | .triangle {
21 | position: absolute;
22 | top: calc(-4.7px - var(--theme-border-size-tip));
23 | left: 50%;
24 | width: 10px;
25 | height: 10px;
26 | transform: translate(-50%, 0) rotate(45deg);
27 | border-top: var(--theme-border-size-tip) solid var(--theme-border-color-tip);
28 | border-left: var(--theme-border-size-tip) solid var(--theme-border-color-tip);
29 | }
30 |
31 | .tip,
32 | .triangle {
33 | background: var(--theme-color-600);
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/ExpansionBox/styles.less:
--------------------------------------------------------------------------------
1 | .container {
2 | position: fixed;
3 | top: 0;
4 | font-size: 12px;
5 | background: var(--theme-color-300);
6 | border: 1px solid var(--theme-color-200);
7 | border-radius: 8px;
8 | color: var(--theme-text-primary);
9 | display: flex;
10 | flex-direction: column;
11 | box-shadow:
12 | 0px 24px 20px rgba(32, 57, 94, 0.05),
13 | 0px 2px 20px rgba(0, 0, 0, 0.08);
14 | z-index: 101;
15 | }
16 |
17 | .container-header {
18 | position: relative;
19 | cursor: move;
20 |
21 | .title {
22 | font-size: 12px;
23 | line-height: 18px;
24 | font-weight: 400;
25 | text-align: center;
26 | margin: 0;
27 | padding: 3px 0;
28 | border-bottom: 1px solid var(--theme-color-350);
29 | white-space: nowrap;
30 | overflow: hidden;
31 | text-overflow: ellipsis;
32 | }
33 |
34 | .close-button {
35 | position: absolute;
36 | top: 2px;
37 | left: 2px;
38 | line-height: 0;
39 | cursor: pointer;
40 | padding: 5px;
41 | border-radius: 6px;
42 |
43 | &:hover {
44 | background: var(--theme-color-200);
45 | }
46 |
47 | svg {
48 | width: 10px;
49 | height: 10px;
50 | }
51 | }
52 | }
53 |
54 | .containers {
55 | position: absolute;
56 | border-radius: 0 0 8px 8px;
57 | height: 100%;
58 | width: 100%;
59 | background-color: #0000;
60 | }
61 |
62 | .anchor {
63 | display: block;
64 | width: var(--anchor-point-size);
65 | height: var(--anchor-point-size);
66 | position: absolute;
67 | background: transparent;
68 | }
69 |
70 | .top {
71 | top: 0px;
72 | left: 0;
73 | width: 100%;
74 | height: 2px;
75 | cursor: ns-resize;
76 | }
77 |
78 | .bottom {
79 | bottom: 0px;
80 | left: 0;
81 | width: 100%;
82 | height: 2px;
83 | cursor: ns-resize;
84 | }
85 |
86 | .left {
87 | left: 0px;
88 | top: 0;
89 | width: 2px;
90 | height: 100%;
91 | cursor: ew-resize;
92 | }
93 |
94 | .right {
95 | right: 0px;
96 | top: 0;
97 | width: 2px;
98 | height: 100%;
99 | cursor: ew-resize;
100 | }
101 |
102 | .top-left {
103 | top: -1px;
104 | left: -1px;
105 | cursor: nwse-resize;
106 | }
107 |
108 | .bottom-left {
109 | bottom: -1px;
110 | left: -1px;
111 | cursor: nesw-resize;
112 | }
113 |
114 | .top-right {
115 | top: -1px;
116 | right: -1px;
117 | cursor: nesw-resize;
118 | }
119 |
120 | .bottom-right {
121 | bottom: -1px;
122 | right: -1px;
123 | cursor: nwse-resize;
124 | }
125 |
126 | .interlayer {
127 | position: absolute;
128 | top: 0;
129 | left: 0;
130 | width: 100%;
131 | height: 100%;
132 | background: transparent;
133 | z-index: 100;
134 | }
135 |
--------------------------------------------------------------------------------
/src/components/IF/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | interface IFProps {
4 | className?: string;
5 | condition: boolean;
6 | forceRender?: boolean;
7 | children: React.ReactNode;
8 | }
9 |
10 | const IF: React.FC = (props) => {
11 | const { className, condition, forceRender, children } = props;
12 |
13 | if (forceRender) {
14 | return condition ? <>{children}> : null;
15 | }
16 |
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | };
23 |
24 | export default React.memo(IF);
25 |
--------------------------------------------------------------------------------
/src/components/Tab/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import classNames from "classnames";
3 | import styles from "./styles.less";
4 |
5 | interface TabProps {
6 | className?: string;
7 | items: Array;
8 | activeIndex?: number;
9 | onChange?: (activeKey: number) => void;
10 | }
11 |
12 | const Tab = React.forwardRef((props, ref) => {
13 | const { className, items, activeIndex, onChange } = props;
14 | const [index, setIndex] = React.useState(activeIndex);
15 |
16 | React.useEffect(() => {
17 | if (typeof activeIndex !== undefined && index !== activeIndex) {
18 | setIndex(index);
19 | }
20 | }, [index, activeIndex]);
21 |
22 | return (
23 |
24 | {items.map((label, idx) => (
25 | {
29 | onChange?.(idx);
30 | setIndex(idx);
31 | }}
32 | >
33 | {label}
34 |
35 | ))}
36 |
37 | );
38 | });
39 |
40 | const areEqual = (prevProps: TabProps, nextProps: TabProps) =>
41 | prevProps.className === nextProps.className &&
42 | prevProps.items === nextProps.items &&
43 | prevProps.onChange === nextProps.onChange;
44 |
45 | export default React.memo(Tab, areEqual);
46 |
--------------------------------------------------------------------------------
/src/components/Tab/styles.less:
--------------------------------------------------------------------------------
1 | .tab {
2 | font-size: 12px;
3 | line-height: 18px;
4 | border-bottom: 1px solid var(--theme-color-50);
5 | }
6 |
7 | .tab .active {
8 | color: var(--theme-text-primary);
9 | border-bottom: 1px solid var(--theme-brand-color);
10 | }
11 |
12 | .tab button {
13 | background: none;
14 | outline: none;
15 | border: none;
16 | padding-bottom: 5px;
17 | color: var(--theme-color-g400);
18 | margin-bottom: -1px;
19 | }
20 |
21 | .tab button:nth-child(n + 2) {
22 | margin-left: 16px;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Tooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import classNames from "classnames";
4 | import styles from "./styles.less";
5 |
6 | interface TipProps {
7 | left: number;
8 | top: number;
9 | tipText: string;
10 | visible: boolean;
11 | shortcutKey?: string[];
12 | }
13 |
14 | interface TooltipProps {
15 | className?: string;
16 | icon?: React.ReactNode;
17 | tipText: string;
18 | shortcutKey?: string[];
19 | onClick?: (e: React.MouseEvent) => void;
20 | }
21 |
22 | const Tip: React.FC = ({ visible, left, top, tipText, shortcutKey = [] }) => {
23 | const [horizontalOffset, setHorizontalOffset] = React.useState(0);
24 | const containerRef = React.useRef();
25 |
26 | React.useEffect(() => {
27 | if (visible) {
28 | let offset = 0;
29 | const bodyWidth = document.body.offsetWidth;
30 | const width = containerRef.current.offsetWidth / 2;
31 | if (left < width) {
32 | offset = width - left;
33 | } else if (left + width > bodyWidth) {
34 | offset = bodyWidth - (left + width);
35 | }
36 | setHorizontalOffset(offset);
37 | }
38 | }, [left, visible]);
39 |
40 | return ReactDOM.createPortal(
41 |
50 | {tipText}
51 | {shortcutKey.map((key, idx) => (
52 |
53 | {key}
54 | {idx === shortcutKey.length - 1 ? "" : "+"}
55 |
56 | ))}
57 |
63 |
,
64 | document.body,
65 | );
66 | };
67 |
68 | const Tooltip = React.forwardRef((props, ref) => {
69 | const { className, icon, tipText, shortcutKey, onClick } = props;
70 | const [tipVisible, setTipVisible] = React.useState(false);
71 | const position = React.useRef({ x: 0, y: 0 });
72 |
73 | const handleMouseEnter = (e: React.MouseEvent) => {
74 | const rect = (e.target as HTMLElement).getBoundingClientRect();
75 | position.current.x = rect.x + rect.width / 2;
76 | position.current.y = rect.y + rect.height + 9;
77 | setTipVisible(true);
78 | };
79 |
80 | const handleMouseLeave = () => {
81 | setTipVisible(false);
82 | };
83 |
84 | return (
85 |
86 |
93 | {icon}
94 |
95 |
102 |
103 | );
104 | });
105 |
106 | const areEqual = (prevProps: TooltipProps, nextProps: TooltipProps) =>
107 | prevProps.className === nextProps.className &&
108 | prevProps.tipText === nextProps.tipText &&
109 | prevProps.shortcutKey === nextProps.shortcutKey;
110 |
111 | export default React.memo(Tooltip, areEqual);
112 |
--------------------------------------------------------------------------------
/src/components/Tooltip/styles.less:
--------------------------------------------------------------------------------
1 | .tip-icon {
2 | width: 24px;
3 | height: 24px;
4 | border-radius: 4px;
5 | cursor: pointer;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | z-index: 99;
10 | }
11 |
12 | .tip-icon:hover {
13 | background: var(--theme-color-200);
14 | }
15 |
16 | .tip-icon:hover .tip {
17 | visibility: unset;
18 | }
19 |
20 | .tip {
21 | position: absolute;
22 | height: 44px;
23 | padding: 0 16px;
24 | color: white;
25 | font-size: 12px;
26 | border-radius: 8px;
27 | box-sizing: border-box;
28 | display: flex;
29 | align-items: center;
30 | justify-content: space-between;
31 | pointer-events: none;
32 | z-index: 910;
33 | border: var(--theme-border-size-tip) solid var(--theme-border-color-tip);
34 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
35 | transform: translate(-50%, 0);
36 | white-space: nowrap;
37 | }
38 |
39 | .tip .code {
40 | line-height: 18px;
41 | color: #d1d5db;
42 | padding: 1px 6px;
43 | background: #3e495b;
44 | border-radius: 4px;
45 | margin: 0 2px;
46 | }
47 |
48 | .tip .text + .code {
49 | margin-left: 24px;
50 | }
51 |
52 | .triangle {
53 | position: absolute;
54 | top: calc(-4.7px - var(--theme-border-size-tip));
55 | left: 50%;
56 | width: 10px;
57 | height: 10px;
58 | transform: translate(-50%, 0) rotate(45deg);
59 | border-top: var(--theme-border-size-tip) solid var(--theme-border-color-tip);
60 | border-left: var(--theme-border-size-tip) solid var(--theme-border-color-tip);
61 | }
62 |
63 | .tip,
64 | .triangle {
65 | background: var(--theme-color-600);
66 | }
67 |
--------------------------------------------------------------------------------
/src/hooks/useStorageInfo.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo } from "react";
2 |
3 | function useStorageInfo(key: string, defaultValue: T) {
4 | const defaultStr = useMemo(() => JSON.stringify(defaultValue), [defaultValue]);
5 | const storageValue: string = localStorage.getItem(key) || defaultStr;
6 |
7 | const value: T = useMemo(() => JSON.parse(storageValue) || defaultValue, [storageValue]);
8 |
9 | const setValue = useCallback(
10 | (data: T) => {
11 | localStorage.setItem(key, JSON.stringify(data));
12 | },
13 | [key],
14 | );
15 |
16 | return [value, setValue] as const;
17 | }
18 |
19 | export default useStorageInfo;
20 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import * as ReactDOM from "react-dom/client";
3 | import PluginsController from "./plugins-controller";
4 |
5 | const App = () => {
6 | const pluginsController = React.useRef();
7 | const initd = React.useRef(false);
8 | const initTimeout = React.useRef(1);
9 | const iframeRef = React.useRef(null);
10 |
11 | const handleInit = React.useCallback(() => {
12 | iframeRef.current.contentWindow.postMessage({ name: "plugins-inject", path: location.origin + "/main.js" }, "*");
13 | if (!initd.current) {
14 | initTimeout.current = setTimeout(handleInit, 3000);
15 | }
16 | }, []);
17 |
18 | React.useEffect(() => {
19 | const onMessage = (event) => {
20 | if (event.data && event.data.name === "plugins-inject-success") {
21 | initd.current = true;
22 | if (initTimeout.current) {
23 | clearTimeout(initTimeout.current);
24 | initTimeout.current = null;
25 | }
26 | }
27 | if (event.data && event.data.name === "plugins-unmounted") {
28 | initd.current = false;
29 | handleInit();
30 | }
31 | };
32 | window.addEventListener("message", onMessage, false);
33 | return () => {
34 | window.removeEventListener("message", onMessage, false);
35 | };
36 | }, [handleInit]);
37 |
38 | useEffect(() => {
39 | pluginsController.current = PluginsController;
40 | setTimeout(handleInit, 1000);
41 | }, [handleInit]);
42 |
43 | return ;
44 | };
45 |
46 | const root = ReactDOM.createRoot(document.getElementById("root"));
47 | root.render( );
48 |
--------------------------------------------------------------------------------
/src/lib/client-info.ts:
--------------------------------------------------------------------------------
1 | export const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
2 |
--------------------------------------------------------------------------------
/src/lib/code-hash.json:
--------------------------------------------------------------------------------
1 | {
2 | "[motion_movesteps, motion_movegrids,motion_turnright,motion_turnleft,motion_gotoxy,motion_glideto,motion_sety,motion_changeyby,motion_setx,motion_changexby,motion_pointindirection,motion_glidesecstoxy,looks_goforwardbackwardlayers,looks_seteffectto,looks_changeeffectby,looks_setsizeto,looks_changesizeby,sound_setvolumeto,sound_changevolumeby,sound_seteffectto,sound_changeeffectby,data_listcontainsitem,data_itemnumoflist,data_itemoflist,data_replaceitemoflist,data_insertatlist,data_deleteoflist,data_addtolist,operator_mathop,operator_round,operator_mod,operator_equals,operator_lt,operator_gt,operator_random,operator_divide,operator_multiply,operator_subtract,operator_add,askandwait,event_whengreaterthan]": "[operator_add,operator_subtract,operator_multiply,operator_divide,operator_random,data_variable,data_listcontents,data_itemoflist,data_itemnumoflist,data_lengthoflist,operator_mod,operator_round,operator_mathop,operator_length,xposition,yposition,direction,costumenumbername,backdropnumbername,size,volume,sensing_distanceto,sensing_mousex,sensing_mousey,loudness,timer,of,current,sensing_dayssince2000,sensing_username,operator_join,operator_letter_of]",
3 | "[looks_think,looks_thinkforsecs,looks_say,looks_sayforsecs,looks_switchbackdropto,switchcostumeto,askandwait,operator_join,operator_letter_of,operator_length,operator_contains]": "[operator_join,operator_letter_of,operator_length,answer,data_variable,data_listcontents,data_itemoflist,data_itemnumoflist,data_lengthoflist,operator_add,operator_subtract,operator_multiply,operator_divide,operator_random,xposition,yposition,direction,costumenumbername,backdropnumbername,size,volume,sensing_distanceto,sensing_mousex,sensing_mousey,loudness,timer,of,current,sensing_dayssince2000,sensing_username]",
4 | "[control_if,control_if_else,wait_until,repeat_until]": "[operator_gt,operator_lt,operator_equals,operator_and,operator_or,operator_not,operator_contains,data_listcontainsitem,sensing_touchingobject,sensing_touchingcolor,sensing_coloristouchingcolor,sensing_keypressed,sensing_mousedown]",
5 | "[event_whenflagclicked,event_whenbroadcastreceived,event_whenkeypressed,event_whenthisspriteclicked,event_whenbackdropswitchesto,event_whengreaterthan,]": "[data_setvariableto,data_changevariableby,looks_show,looks_hide,motion_gotoxy,motion_goto,control_forever,control_repeat,repeat_until,control_if,control_if_else,control_wait,data_deletealloflist]",
6 | "[control_forever]": "[control_if,control_if_else]",
7 | "[data_setvariableto,data_changevariableby,control_wait,control_repeat]": "[data_setvariableto,data_changevariableby,control_forever,control_repeat,repeat_until,control_if,control_if_else,control_wait,operator_add,operator_subtract,operator_multiply,operator_divide,operator_random,data_variable,data_listcontents,data_itemoflist,data_itemnumoflist,data_lengthoflist,operator_mod,operator_round,operator_mathop,operator_length,xposition,yposition,direction,costumenumbername,backdropnumbername,size,volume,sensing_distanceto,sensing_mousex,sensing_mousey,loudness,timer,of,current,sensing_dayssince2000,sensing_username,operator_join,operator_letter_of]",
8 | "[operator_or,operator_and,operator_not]": "[sensing_touchingobject,sensing_touchingcolor,sensing_coloristouchingcolor,sensing_keypressed,sensing_mousedown,operator_gt,operator_lt,operator_equals,operator_and,operator_or,operator_not]"
9 | }
10 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import PluginsController from "./plugins-controller";
2 | import Plugins from "./plugins-entry";
3 |
4 | export const PluginNames = Object.keys(Plugins);
5 | export { PluginsController as default };
6 |
--------------------------------------------------------------------------------
/src/plugins-entry.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prettier/prettier */
2 | export default {
3 | folder: () => import(/* webpackChunkName: "plugin-folder" */ "src/plugins/folder"),
4 | "code-find": () => import(/* webpackChunkName: "plugin-code-find" */ "src/plugins/code-find"),
5 | "code-filter": () => import(/* webpackChunkName: "plugin-code-filter" */ "src/plugins/code-filter"),
6 | "dev-tools": () => import(/* webpackChunkName: "plugin-dev-tools" */ "src/plugins/dev-tools"),
7 | "code-switch": () => import(/* webpackChunkName: "plugin-code-switch" */ "src/plugins/code-switch"),
8 | terminal: () => import(/* webpackChunkName: "plugin-terminal" */ "src/plugins/terminal"),
9 | "code-batch-select": () => import(/* webpackChunkName: "plugin-code-batch-select" */ "src/plugins/code-batch-select"),
10 | "dropdown-searchable": () =>
11 | import(/* webpackChunkName: "plugin-dropdown-searchable" */ "src/plugins/dropdown-searchable"),
12 | statistics: () => import(/* webpackChunkName: "plugin-statistics" */ "src/plugins/statistics"),
13 | "historical-version": () =>
14 | import(/* webpackChunkName: "plugin-historical-version" */ "src/plugins/historical-version"),
15 | "custom-plugin": () => import(/* webpackChunkName: "plugin-custom-plugin" */ "src/plugins/custom-plugin"),
16 | "witcat-blockinput": () => import(/* webpackChunkName: "plugin-witcat-blockinput" */ "src/plugins/witcat-blockinput"),
17 | "kukemc-beautify": () => import(/* webpackChunkName: "plugin-kukemc-beautify" */ "src/plugins/kukemc-beautify"),
18 | "fast-input": () => import(/* webpackChunkName: "plugin-fast-input" */ "src/plugins/fast-input"),
19 | "better-sprite-menu": () => import(/* webpackChunkName: "plugin-better-sprite-menu" */ "plugins/better-sprite-menu"),
20 | inspiro: () => import(/* webpackChunkName: "plugin-inspiro" */ "src/plugins/inspiro"),
21 | "custom-css": () => import(/* webpackChunkName: "plugin-custom-css" */ "src/plugins/custom-css"),
22 | "extension-manager": () => import(/* webpackChunkName: "plugin-extension-manager" */ "src/plugins/extension-manager"),
23 | "voice-cooperation": () => import(/* webpackChunkName: "plugin-voice-cooperation" */ "src/plugins/voice-cooperation"),
24 | "block-sharing": () => import(/* webpackChunkName: "plugin-block-sharing" */ "plugins/block-sharing"),
25 | "costume-piskel": () => import(/* webpackChunkName: "plugin-costume-piskel" */ "src/plugins/costume-piskel"),
26 | "data-category-tweaks": () => import(/* webpackChunkName: "plugin-data-category-tweaks" */ "plugins/data-category-tweaks"),
27 | "mobile-code-batch-select": () => import(/* webpackChunkName: "plugin-mobile-code-batch-select" */ "src/plugins/mobile-code-batch-select"),
28 | "clean-pro": () => import(/* webpackChunkName: "plugin-clean-pro" */ "src/plugins/clean-pro"),
29 | } as const;
30 |
--------------------------------------------------------------------------------
/src/plugins-l10n.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | en: require("./l10n/en.json"),
3 | es: require("./l10n/es.json"),
4 | ms: require("./l10n/ms.json"),
5 | ru: require("./l10n/ru.json"),
6 | uk: require("./l10n/uk.json"),
7 | "zh-cn": require("./l10n/zh-cn.json"),
8 | };
9 |
--------------------------------------------------------------------------------
/src/plugins-manifest.ts:
--------------------------------------------------------------------------------
1 | import folder from "src/plugins/folder/manifest";
2 | import codeFind from "src/plugins/code-find/manifest";
3 | import codeFilter from "src/plugins/code-filter/manifest";
4 | import devTools from "src/plugins/dev-tools/manifest";
5 | import codeSwitch from "src/plugins/code-switch/manifest";
6 | import terminal from "src/plugins/terminal/manifest";
7 | import codeBatchSelect from "src/plugins/code-batch-select/manifest";
8 | import dropdownSearchable from "src/plugins/dropdown-searchable/manifest";
9 | import statistics from "src/plugins/statistics/manifest";
10 | import historicalVersion from "src/plugins/historical-version/manifest";
11 | import customPlugin from "src/plugins/custom-plugin/manifest";
12 | import witcatBlockinput from "src/plugins/witcat-blockinput/manifest";
13 | import kukemcBeautify from "src/plugins/kukemc-beautify/manifest";
14 | import fastInput from "src/plugins/fast-input/manifest";
15 | import BetterSpriteMenu from "plugins/better-sprite-menu/manifest";
16 | import inspiro from "plugins/inspiro/manifest";
17 | import customCss from "src/plugins/custom-css/manifest";
18 | import extensionManager from "src/plugins/extension-manager/manifest";
19 | import voiceCooperation from "src/plugins/voice-cooperation/manifest";
20 | import blockSharing from "plugins/block-sharing/manifest";
21 | import costumePiskel from "src/plugins/costume-piskel/manifest";
22 | import dataCategoryTweaks from "plugins/data-category-tweaks/manifest";
23 | import mobileCodeBatchSelect from "src/plugins/mobile-code-batch-select/manifest";
24 | import cleanPro from "src/plugins/clean-pro/manifest";
25 |
26 | export default {
27 | folder,
28 | "code-find": codeFind,
29 | "code-filter": codeFilter,
30 | "dev-tools": devTools,
31 | "code-switch": codeSwitch,
32 | terminal,
33 | "code-batch-select": codeBatchSelect,
34 | "dropdown-searchable": dropdownSearchable,
35 | statistics,
36 | "historical-version": historicalVersion,
37 | "custom-plugin": customPlugin,
38 | "witcat-blockinput": witcatBlockinput,
39 | "kukemc-beautify": kukemcBeautify,
40 | "fast-input": fastInput,
41 | "better-sprite-menu": BetterSpriteMenu,
42 | inspiro,
43 | "custom-css": customCss,
44 | "extension-manager": extensionManager,
45 | "voice-cooperation": voiceCooperation,
46 | "block-sharing": blockSharing,
47 | "costume-piskel": costumePiskel,
48 | "data-category-tweaks": dataCategoryTweaks,
49 | "mobile-code-batch-select": mobileCodeBatchSelect,
50 | "clean-pro": cleanPro,
51 | };
52 |
--------------------------------------------------------------------------------
/src/plugins/better-sprite-menu/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import BetterSpriteMenuIcon from 'assets/icon--BetterSpriteMenu.svg';
3 | import styles from './styles.less'
4 |
5 | let currentSpriteMenuLayout = 'default'
6 | const menuLayoutList = ["default", "grid", "compact", "superCompact"]
7 |
8 | const removeAllStyles = () => {
9 | document.body.classList.remove(styles.grid);
10 | document.body.classList.remove(styles.compact);
11 | document.body.classList.remove(styles.superCompact);
12 | }
13 | const updateSpriteMenuStyle = () => {
14 | removeAllStyles()
15 | switch(currentSpriteMenuLayout) {
16 | case "grid":
17 | document.body.classList.add(styles.grid);
18 | //stageInit()
19 | break;
20 | case "compact":
21 | document.body.classList.add(styles.compact);
22 | break;
23 | case "superCompact":
24 | document.body.classList.add(styles.superCompact);
25 | break;
26 | }
27 | }
28 |
29 | let collapsibleBox = document.querySelectorAll('.gandi_collapsible-box_collapsible-box_1_329')[1];
30 |
31 | // Create a new MutationObserver instance
32 | let observer = new MutationObserver(function(mutations) {
33 | mutations.forEach(function(mutation) {
34 | if (mutation.attributeName === "class" && currentSpriteMenuLayout == "grid") {
35 | let targetElement = mutation.target as Element;
36 | let newClassList = targetElement.className.split(' ');
37 |
38 | if (newClassList.includes('gandi_collapsible-box_collapsed_oQuU1')) {
39 | console.log('The element contains the class "gandi_collapsible-box_collapsed_oQuU1"');
40 | removeAllStyles();
41 | } else {
42 | updateSpriteMenuStyle()
43 | }
44 | }
45 | });
46 | });
47 |
48 | let config = { attributes: true, attributeFilter: ['class'] };
49 | observer.observe(collapsibleBox, config);
50 |
51 |
52 | const BetterSpriteMenu: React.FC = ({ redux, msg, registerSettings}) => {
53 | React.useEffect(() => {
54 | currentSpriteMenuLayout = 'default'
55 | const register = registerSettings(
56 | msg('plugins.betterSpriteMenu.title'),
57 | 'Better Sprite Menu',
58 | [
59 | {
60 | key: 'layouts',
61 | label: msg('plugins.betterSpriteMenu.title'),
62 | description: msg("plugins.betterSpriteMenu.description"),
63 | items: [
64 | {
65 | key: 'layout',
66 | type: 'select',
67 | label: msg('plugins.betterSpriteMenu.layouts.label'),
68 | value: currentSpriteMenuLayout,
69 | options: [
70 | { label: msg('plugins.betterSpriteMenu.layouts.default'), value: menuLayoutList[0] },
71 | { label: msg('plugins.betterSpriteMenu.layouts.grid'), value: menuLayoutList[1] },
72 | { label: msg('plugins.betterSpriteMenu.layouts.compact'), value: menuLayoutList[2] },
73 | { label: msg('plugins.betterSpriteMenu.layouts.superCompact'), value: menuLayoutList[3] },
74 | ],
75 | onChange: (value) => {
76 | currentSpriteMenuLayout = value.toString();
77 | updateSpriteMenuStyle();
78 | },
79 | },
80 | ],
81 | },
82 | ],
83 | ,
84 | );
85 | return () => {
86 | removeAllStyles()
87 | register.dispose();
88 | };
89 | }, [registerSettings, msg]);
90 |
91 | // Use the layout style in your render method
92 | return null;
93 | };
94 |
95 | BetterSpriteMenu.displayName = 'BetterSpriteMenu';
96 |
97 | export default BetterSpriteMenu;
98 |
--------------------------------------------------------------------------------
/src/plugins/better-sprite-menu/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "better-sprite-menu",
3 | type: "component",
4 | description: "New varieties of sprite menu layout.",
5 | credits: [
6 | {
7 | name: "fath11",
8 | link: "https://cocrea.world/@Fath11",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/components/Article.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles.less";
2 | import React from "react";
3 | import classNames from "classnames";
4 |
5 | interface ArticleProps {
6 | content: { url: string; block: string };
7 | }
8 |
9 | const Article: React.FC = ({ content }) => {
10 | return (
11 |
16 | );
17 | };
18 |
19 | export default Article;
20 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/components/ArticleList.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles.less";
2 | import React from "react";
3 | import Article from "./Article";
4 |
5 | interface ArticleListProps {
6 | list: Array<{ url: string; block: string }>;
7 | msg: (key: string) => string;
8 | }
9 |
10 | const ArticleList: React.FC = ({ list, msg }) => {
11 | return (
12 |
13 | {list.length === 0 ? (
14 |
{msg("plugins.blockSharing.noArticle")}
15 | ) : (
16 | list.map((item, index) =>
)
17 | )}
18 |
19 | );
20 | };
21 |
22 | export default ArticleList;
23 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/components/BluePrint.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles.less";
2 | import React, { useRef } from "react";
3 | import { Spinner } from "@gandi-ide/gandi-ui";
4 | import classNames from "classnames";
5 |
6 | interface BluePrintProps {
7 | content: {
8 | url: string;
9 | block: string;
10 | detail?: { creationRelease: { coverGifLink: number; coverLink: number }; description: string; type: Array };
11 | };
12 | msg: (key: string) => string;
13 | }
14 |
15 | const BluePrint: React.FC = ({ content, msg }) => {
16 | const imgRef = useRef(null);
17 | const [loading, setLoading] = React.useState(true);
18 |
19 | const dragstart = () => {
20 | if (content.detail && content.detail.type) {
21 | window.postMessage(["startDrop", content.detail], "*");
22 | } else {
23 | window.postMessage(["startDrop", String(content.url)], "*");
24 | }
25 | };
26 |
27 | const dragend = (e: React.DragEvent) => {
28 | window.postMessage(["cancelDrop", [e.clientX, e.clientY]], "*");
29 | };
30 |
31 | const load = () => {
32 | setLoading(false);
33 | };
34 |
35 | return (
36 |
37 | {loading && (
38 |
39 |
40 |
41 | )}
42 |
53 |
54 | );
55 | };
56 |
57 | export default BluePrint;
58 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/components/BluePrintList.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles.less";
2 | import React from "react";
3 | import BluePrint from "./BluePrint";
4 |
5 | interface BluePrintListProps {
6 | list: Array<{ url: string; block: string }>;
7 | msg: (key: string) => string;
8 | }
9 |
10 | const BluePrintList: React.FC = ({ list, msg }) => {
11 | return (
12 |
13 | {list.length === 0 ? (
14 |
{msg("plugins.blockSharing.noBluePrint")}
15 | ) : (
16 | list.map((item, index) =>
)
17 | )}
18 |
19 | );
20 | };
21 |
22 | export default BluePrintList;
23 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/components/Demo.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles.less";
2 | import React, { useRef } from "react";
3 | import { Spinner } from "@gandi-ide/gandi-ui";
4 | import classNames from "classnames";
5 |
6 | interface BluePrintProps {
7 | content: {
8 | url: string;
9 | block: string;
10 | detail?: { creationRelease: { coverGifLink: number; coverLink: number }; description: string; type: Array };
11 | };
12 | }
13 |
14 | const BluePrint: React.FC = ({ content }) => {
15 | const imgRef = useRef(null);
16 | const [loading, setLoading] = React.useState(true);
17 | const load = () => {
18 | setLoading(false);
19 | };
20 |
21 | const handleClick = () => {
22 | window.open(`${content.url}?remixing=true`, "_blank");
23 | };
24 |
25 | return (
26 |
27 | {loading && (
28 |
29 |
30 |
31 | )}
32 |
46 |
47 | );
48 | };
49 |
50 | export default BluePrint;
51 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/components/DemoList.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles.less";
2 | import React from "react";
3 | import Demo from "./Demo";
4 |
5 | interface BluePrintListProps {
6 | list: Array<{ url: string; block: string }>;
7 | msg: (key: string) => string;
8 | }
9 |
10 | const BluePrintList: React.FC = ({ list, msg }) => {
11 | return (
12 |
13 | {list.length === 0 ? (
14 |
{msg("plugins.blockSharing.noDemo")}
15 | ) : (
16 | list.map((item, index) =>
)
17 | )}
18 |
19 | );
20 | };
21 |
22 | export default BluePrintList;
23 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/components/Home.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles.less";
2 | import React, { useRef } from "react";
3 | import { Spinner } from "@gandi-ide/gandi-ui";
4 | import hack from "../hack";
5 |
6 | interface ArticleProps {
7 | name: string;
8 | Jump: string;
9 | }
10 |
11 | const Article: React.FC = ({ name, Jump }) => {
12 | const iframeRef = useRef(null);
13 | const [loading, setLoading] = React.useState(true);
14 |
15 | React.useEffect(() => {
16 | hack.setLoad(setLoading);
17 | if (iframeRef.current) {
18 | iframeRef.current.onload = () => {
19 | hack.bluePrint = [];
20 | hack.article = [];
21 | hack.demo = [];
22 | // 在这里执行你的加载完成后的逻辑
23 | setTimeout(() => {
24 | setTimeout(() => {
25 | if (window.location.search.indexOf("Block") !== -1) {
26 | iframeRef.current.contentWindow.postMessage(["loadBlock"], "*");
27 | }
28 | }, 2000);
29 | setLoading(false);
30 | }, 100);
31 | };
32 | }
33 | }, []);
34 |
35 | return (
36 |
37 | {loading && (
38 |
39 |
40 |
41 | )}
42 |
52 |
53 | );
54 | };
55 |
56 | export default Article;
57 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/icons/home.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "block-sharing",
3 | type: "component",
4 | description: "为编程提供更多可能",
5 | credits: [
6 | {
7 | name: "白猫@CCW",
8 | link: "https://www.ccw.site/student/6173f57f48cf8f4796fc860e",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/material-drag.ts:
--------------------------------------------------------------------------------
1 | // 整理拖拽的数据源,以及传递到GUI。
2 | // eslint-disable-next-line require-jsdoc
3 | export default function materialDrag(data: any): void {
4 | const dragData = {
5 | ...data,
6 | ...(data.payload && {
7 | payload: {
8 | ...data.payload,
9 | bodyUrl: data.payload.uri,
10 | body: data.payload.uri.substring(data.payload.uri.lastIndexOf("/") + 1),
11 | },
12 | }),
13 | };
14 | return dragData;
15 | }
16 |
--------------------------------------------------------------------------------
/src/plugins/block-sharing/styles.less:
--------------------------------------------------------------------------------
1 | .container {
2 | position: relative;
3 | border-radius: 0 0 8px 8px;
4 | font-size: 12px;
5 | line-height: 18px;
6 | padding: 9px;
7 | color: var(--theme-color-g300);
8 | height: calc(100% - 25px);
9 | }
10 |
11 | .tab-wrapper {
12 | height: calc(100% - 15px);
13 | overflow: hidden;
14 | }
15 |
16 | .tab {
17 | margin: 0px;
18 | display: flex;
19 | flex-grow: 1; // 使 Tab 占据剩余空间
20 | }
21 |
22 | .icon {
23 | display: flex;
24 | align-items: center;
25 | justify-content: center;
26 | width: 32px !important;
27 | height: 32px !important;
28 | border-radius: 4px;
29 | cursor: pointer;
30 | }
31 |
32 | .inner {
33 | height: calc(100% - 10px);
34 | color: var(--theme-text-primary);
35 | overflow: hidden;
36 | }
37 |
38 | .loading {
39 | position: absolute;
40 | width: 20px;
41 | height: 20px;
42 | z-index: 1;
43 | }
44 |
45 | .window {
46 | width: 100%;
47 | height: calc(100% - 14px);
48 | overflow: hidden;
49 | overflow-y: auto;
50 | text-align: center;
51 | color: #000;
52 | background-color: #ffffff;
53 | margin-top: 5px;
54 | }
55 |
56 | .windows {
57 | position: absolute;
58 | display: grid;
59 | place-items: center;
60 | justify-content: center;
61 | align-items: center;
62 | width: calc(100% - 18px);
63 | height: calc(100% - 42px);
64 | background: var(--theme-color-300);
65 | }
66 |
67 | .imgCard {
68 | margin-top: 5px;
69 | margin-bottom: 5px;
70 | position: relative;
71 | left: 10px;
72 | width: calc(100% - 20px);
73 | max-height: 200px;
74 | border-radius: 5px;
75 | transition: all 0.2s linear;
76 | overflow: hidden;
77 | }
78 |
79 | .imgCard:hover {
80 | background: #ffffff;
81 | transform: scale(1.01);
82 | box-shadow: 0px 7px 15px 0px rgba(0, 0, 0, 0.076), 7px 0px 15px 0px rgba(0, 0, 0, 0.076);
83 | }
84 |
85 | .imgCardLoad {
86 | width: 100%;
87 | height: 100%;
88 | border-radius: 5px;
89 | }
90 |
91 | .block {
92 | cursor: move;
93 | }
94 |
95 | .url {
96 | cursor: pointer;
97 | }
98 |
99 | .BackButtonIcon {
100 | display: flex;
101 | }
102 |
103 | .iconBox {
104 | margin-right: 6px;
105 | border-radius: 6px;
106 | transition: all 0.1s linear;
107 | padding: 3px;
108 | width: 20px;
109 | height: 20px;
110 | cursor: pointer;
111 | margin-bottom: 4px;
112 |
113 | &:hover {
114 | background: var(--theme-color-200);
115 | }
116 | }
117 |
118 | .p {
119 | margin-top: 16px;
120 | font-size: 11px;
121 | }
--------------------------------------------------------------------------------
/src/plugins/clean-pro/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styles from "./styles.less";
3 | import ReactMarkdown from "react-markdown";
4 | import CleanProIcon from "../../assets/icon--clean-pro.svg";
5 | const CleanPro: React.FC = ({ intl, vm, workspace, registerSettings, msg }) => {
6 | React.useEffect(() => {
7 | const menuItemId = window.Blockly.ContextMenu.addDynamicMenuItem(
8 | (items, target) => {
9 | items.splice(4, 0, {
10 | id: "cleanPro",
11 | text: msg("plugins.cleanPro.cleanHeadlessBlocks"),
12 | enabled: true,
13 | callback: () => {
14 | const cleanHeadlessBlocks = () => {
15 | const currentTarget = vm.editingTarget;
16 | const blocks = currentTarget.blocks._blocks;
17 |
18 | window.Blockly.Events.setGroup(true);
19 |
20 | // 使用循环直到没有更多块可以删除
21 | let hasRemovedBlocks = true;
22 | while (hasRemovedBlocks) {
23 | hasRemovedBlocks = false;
24 |
25 | // 获取当前的块列表
26 | const blockIds = Object.keys(currentTarget.blocks._blocks);
27 |
28 | // 找出所有顶级块(没有父块的块)
29 | const topBlocks = blockIds.filter((id) => {
30 | const block = currentTarget.blocks._blocks[id];
31 | return !block.parent;
32 | });
33 |
34 | // 遍历所有顶级块
35 | for (const id of topBlocks) {
36 | const block = workspace.getBlockById(id);
37 | if (block) {
38 | const isHat = block.startHat_;
39 | const hasNextBlock = block.getNextBlock() !== null;
40 |
41 | if (!isHat || (isHat && !hasNextBlock)) {
42 | // 删除拼在他下面的blocks
43 | block.getChildren(true).forEach((child) => {
44 | child.dispose(true);
45 | });
46 | block.dispose(true);
47 | hasRemovedBlocks = true;
48 | }
49 | }
50 | }
51 | }
52 |
53 | window.Blockly.Events.setGroup(false);
54 | };
55 |
56 | cleanHeadlessBlocks();
57 | },
58 | });
59 | return items;
60 | },
61 | {
62 | targetNames: ["workspace"],
63 | },
64 | );
65 |
66 | const register = registerSettings(
67 | msg("plugins.cleanPro.title"),
68 | "plugin-clean-pro",
69 | [
70 | {
71 | key: "cleanPro",
72 | label: msg("plugins.cleanPro.title"),
73 |
74 | items: [
75 | {
76 | key: "cleanHeadlessBlocks",
77 | label: "插件文档",
78 | type: "input",
79 | inputProps: {
80 | type: "input",
81 | onFocus: (e) => {
82 | e.target.blur();
83 | },
84 | },
85 | value: " ",
86 | description: {msg("plugins.cleanPro.docs")} ,
87 | onChange: (value: boolean) => {},
88 | },
89 | ],
90 | },
91 | ],
92 | ,
93 | );
94 |
95 | return () => {
96 | window.Blockly.ContextMenu.deleteDynamicMenuItem(menuItemId);
97 | register.dispose();
98 | };
99 | }, [vm, workspace]);
100 |
101 | return {"Clean Pro Plugin"} ;
102 | };
103 |
104 | CleanPro.displayName = "CleanPro";
105 |
106 | export default CleanPro;
107 |
--------------------------------------------------------------------------------
/src/plugins/clean-pro/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "clean-pro",
3 | type: "component",
4 | description: "Clean Up Blocks Pro!! Quickly clear annoying headless blocks~",
5 | credits: [
6 | {
7 | name: "多bug的啸天犬 @ CCW",
8 | link: "https://www.ccw.site/student/197354885",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/code-batch-select/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Code Batch Select",
3 | type: "component",
4 | description: "Helps you batch select code",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/code-batch-select/styles.less:
--------------------------------------------------------------------------------
1 | .code {
2 | line-height: 18px;
3 | color: #d1d5db;
4 | padding: 1px 6px;
5 | background: #3e495b;
6 | border-radius: 4px;
7 | margin: 0 2px;
8 | }
--------------------------------------------------------------------------------
/src/plugins/code-batch-select/useKeyDownOperate.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { isCtrlKeyDown } from "utils/index";
3 | import { copyBatchedElements, pasteBatchedElements } from "utils/block-helper";
4 | import { SelectedElements } from "./useBatchSelect";
5 |
6 | const useKeyDownOperate: (params: { blockly: any; workspace: Blockly.WorkspaceSvg; vm: VirtualMachine }) => null = ({
7 | blockly,
8 | workspace,
9 | vm,
10 | }) => {
11 | const mousemoveRef = useRef<{ clientX: number; clientY: number }>({
12 | clientX: 0,
13 | clientY: 0,
14 | });
15 |
16 | const mousemove = (e) => {
17 | mousemoveRef.current = { clientX: e.clientX, clientY: e.clientY };
18 | };
19 |
20 | const onKeyDown = (e) => {
21 | if (blockly.locked) {
22 | return;
23 | }
24 | let batchDeleteBlocks = false;
25 | let batchCopyBlocks = false;
26 |
27 | const batchSelectedElements: SelectedElements = blockly.batchSelectedElements || [{}, {}];
28 | const selectedBlocks = Object.values(batchSelectedElements[0]);
29 | const selectedFrames = Object.values(batchSelectedElements[1]);
30 | const selectedElements = [...selectedBlocks, ...selectedFrames];
31 | if (e.keyCode === 8 || e.keyCode === 46) {
32 | if (selectedElements.length === 0) {
33 | // 没有选中的元素则return
34 | return;
35 | }
36 | // delete and backspace
37 | batchDeleteBlocks = true;
38 | }
39 | if (isCtrlKeyDown(e)) {
40 | if (e.keyCode == 86 && blockly.clipboardBatchElements?.length > 0) {
41 | // 'ctrl + v'
42 | pasteBatchedElements(mousemoveRef.current, workspace, blockly.clipboardBatchElements, vm);
43 | } else if (selectedElements.length === 0) {
44 | // 没有批量选中的block,并且按键按到了ctrl c 或者ctrl v 清除已记录的值。
45 | if (e.keyCode === 67 || e.keyCode === 88) {
46 | blockly.clipboardBatchElements = [];
47 | }
48 | return;
49 | } else if (e.keyCode === 67) {
50 | // 'ctrl + c'
51 | batchCopyBlocks = true;
52 | } else if (e.keyCode === 88) {
53 | // 'ctrl + x'
54 | batchCopyBlocks = true;
55 | batchDeleteBlocks = true;
56 | }
57 | }
58 |
59 | if (batchCopyBlocks) {
60 | blockly.clipboardBatchElements = copyBatchedElements(blockly.batchSelectedElements as SelectedElements);
61 | }
62 |
63 | if (batchDeleteBlocks) {
64 | blockly.Events.setGroup(true);
65 | selectedBlocks.forEach((bl) => {
66 | setTimeout(function () {
67 | blockly.mainWorkspace.fireDeletionListeners(bl);
68 | });
69 | bl.dispose(true, true);
70 | });
71 | selectedFrames.forEach((frame) => {
72 | frame.dispose();
73 | });
74 | blockly.batchSelectedElements = null;
75 | blockly.Events.setGroup(false);
76 | }
77 | };
78 |
79 | useEffect(() => {
80 | document.addEventListener("keydown", onKeyDown);
81 | document.addEventListener("mousemove", mousemove);
82 | return () => {
83 | document.removeEventListener("keydown", onKeyDown);
84 | document.addEventListener("mousemove", mousemove);
85 | };
86 | }, []);
87 |
88 | return null;
89 | };
90 |
91 | export default useKeyDownOperate;
92 |
--------------------------------------------------------------------------------
/src/plugins/code-batch-select/useRightContextMenu.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import type { IntlShape } from "react-intl";
3 | import { copyBatchedElements, pasteBatchedElements } from "utils/block-helper";
4 | import { SelectedElements } from "./useBatchSelect";
5 |
6 | interface IProps {
7 | workspace: Blockly.WorkspaceSvg;
8 | blockly: any;
9 | clearAllBoxedElements?: (boolean) => void;
10 | intl: IntlShape;
11 | vm: VirtualMachine;
12 | }
13 |
14 | const useBatchSelectRightMenu = ({ workspace, blockly, clearAllBoxedElements, intl, vm }: IProps) => {
15 | useEffect(() => {
16 | const menuItemId = window.Blockly.ContextMenu.addDynamicMenuItem(
17 | (items, element) => {
18 | if (element.boxed) {
19 | const menus = [
20 | {
21 | id: "Copy all",
22 | text: intl.formatMessage({
23 | id: "plugins.codeBatchSelect.duplicate.all",
24 | }),
25 | enabled: true,
26 | callback: () => {
27 | if (!blockly.batchSelectedElements) return;
28 | blockly.clipboardBatchElements = copyBatchedElements(blockly.batchSelectedElements as SelectedElements);
29 | },
30 | },
31 | {
32 | id: "Delete all",
33 | text: intl.formatMessage({
34 | id: "plugins.codeBatchSelect.delete.all",
35 | }),
36 | enabled: true,
37 | callback: () => {
38 | if (!blockly.batchSelectedElements) return;
39 | blockly.Events.setGroup(true);
40 | Object.values(blockly.batchSelectedElements[0]).forEach((bl: Blockly.Block) => {
41 | setTimeout(function () {
42 | blockly.mainWorkspace.fireDeletionListeners(bl);
43 | });
44 | bl.dispose(true, true);
45 | });
46 | Object.values(blockly.batchSelectedElements[1]).forEach((frame: Blockly.Frame) => {
47 | frame.dispose();
48 | });
49 | blockly.Events.setGroup(false);
50 | clearAllBoxedElements(true);
51 | },
52 | },
53 | ];
54 | return items.splice(0, items.length, ...menus);
55 | }
56 | return items;
57 | },
58 | {
59 | targetNames: ["frame", "blocks"],
60 | },
61 | );
62 | return () => {
63 | window.Blockly.ContextMenu.deleteDynamicMenuItem(menuItemId);
64 | };
65 | }, []);
66 |
67 | useEffect(() => {
68 | const menuItemId = window.Blockly.ContextMenu.addDynamicMenuItem(
69 | (items, _, event) => {
70 | if (blockly.clipboardBatchElements?.length > 0) {
71 | items.splice(2, 0, {
72 | id: "Paste all",
73 | text: intl.formatMessage({
74 | id: "plugins.codeBatchSelect.paste.all",
75 | }),
76 | enabled: true,
77 | callback: () => {
78 | pasteBatchedElements(event, workspace, blockly.clipboardBatchElements, vm);
79 | },
80 | });
81 | }
82 | return items;
83 | },
84 | {
85 | targetNames: ["workspace"],
86 | },
87 | );
88 | return () => {
89 | window.Blockly.ContextMenu.deleteDynamicMenuItem(menuItemId);
90 | };
91 | }, []);
92 | return null;
93 | };
94 |
95 | export default useBatchSelectRightMenu;
96 |
--------------------------------------------------------------------------------
/src/plugins/code-filter/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Code filter",
3 | type: "component",
4 | description: "Helps you find and add code quickly.",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/code-filter/styles.less:
--------------------------------------------------------------------------------
1 | .code-search-container {
2 | position: absolute;
3 | left: 50%;
4 | top: 50%;
5 | z-index: 101;
6 | }
7 |
8 | .code-search-body {
9 | padding: 8px;
10 | font-size: 12px;
11 | background: #1d2634;
12 | border: 1px solid var(--theme-color-200);
13 | border-radius: 8px;
14 | color: var(--theme-text-primary);
15 | display: flex;
16 | flex-direction: column;
17 | }
18 |
19 | .icon {
20 | position: absolute;
21 | right: 3px;
22 | top: 2px;
23 | }
24 |
25 | .container-body {
26 | padding: 5px 8px;
27 | overflow: hidden;
28 | max-height: calc(100% - 31px);
29 | display: flex;
30 | flex-direction: column;
31 | // background: var(--theme-color-550);
32 | }
33 |
34 | .search-input {
35 | flex-shrink: 0;
36 | color: var(--theme-text-primary);
37 | width: 100%;
38 | font-size: 12px;
39 | font-weight: 400;
40 | padding: 8px 8px;
41 | outline: none;
42 | background: var(--theme-color-500);
43 | border: 1px solid var(--theme-color-200);
44 | border-radius: 6px;
45 | overflow: hidden;
46 | text-overflow: ellipsis;
47 | white-space: nowrap;
48 | }
49 |
50 | .search-input:focus {
51 | border: 1px solid var(--theme-brand-color);
52 | }
53 |
54 | .search-input::placeholder {
55 | color: #6b7280;
56 | }
57 |
58 | .options {
59 | margin: 8px 0;
60 | padding: 0;
61 | user-select: none;
62 | overflow-y: auto;
63 | overflow-x: hidden;
64 | }
65 | .options::-webkit-scrollbar {
66 | width: 7px;
67 | background-color: transparent;
68 | }
69 |
70 | .options::-webkit-scrollbar-thumb {
71 | border-radius: 6px;
72 | border: 1px solid rgba(255, 255, 255, 0.3);
73 | }
74 |
75 | .options li {
76 | padding: 3px 8px;
77 | position: relative;
78 | cursor: pointer;
79 | list-style: none;
80 | display: flex;
81 | cursor: grab;
82 | }
83 |
84 | .options li:hover,
85 | .options li.selected {
86 | background-color: rgba(156, 163, 175, 0.1);
87 | }
88 |
89 | .ghost {
90 | display: none;
91 | }
92 |
--------------------------------------------------------------------------------
/src/plugins/code-find/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Code find",
3 | type: "component",
4 | description: "Helps you quickly find the current target's code and highlight it.",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/code-switch/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Code Switch",
3 | type: "component",
4 | description: "Code Switch",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/costume-piskel/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "costume-piskel",
3 | type: "component",
4 | description: "Create pixel-style materials will become easier.",
5 | credits: [
6 | {
7 | name: "Cappu",
8 | link: "https://www.ccw.site/student/62b963fe01fb2c3c96d5d8ad",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/custom-css/PresetThemesList.md:
--------------------------------------------------------------------------------
1 | # Preset Themes List
2 |
3 | This is a list of all the preset themes in the Custom CSS plugin.
4 |
5 | ### Preset Themes
6 |
7 | 1.turbowarpDark (BY Fath11)
8 |
9 | 2.penguinmodDark (BY Fath11)
10 |
11 | 3.暗紫杏黄 (By 多bug的啸天犬) #130F23 #B9AE80
12 |
13 | 4. 霜雪蓝冰 (By 多bug的啸天犬) #E8F1F5 #0FB1CA
14 |
15 | 5. 豆青乱玄 (By 多bug的啸天犬) #92CD55 #3C3B50
16 |
17 | 6. Some auto themes
18 | > { id: "oceanBlue", color1: "#1A3A5A", color2: "#00BFFF" },
19 | > { id: "mintChocolate", color1: "#2D3436", color2: "#6ACDAF" },
20 | > { id: "sunsetOrange", color1: "#2D3436", color2: "#FF7675" },
21 | > { id: "greatPurpleAsBlue", color1: "#402F64", color2: "#70BED2" },
22 |
--------------------------------------------------------------------------------
/src/plugins/custom-css/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "custom-css",
3 | type: "function",
4 | description: "Load your own css files!Customize the Gandi style!",
5 | credits: [
6 | {
7 | name: "多bug的啸天犬 @ CCW",
8 | link: "https://www.ccw.site/student/197354885",
9 | },
10 | {
11 | name: "fath11@cocrea",
12 | link: "https://cocrea.world/@Fath11",
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/custom-plugin/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "custom-plugin",
3 | type: "component",
4 | description: "Supports a more flexible way to load plug-ins",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/data-category-tweaks/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Data category tweaks",
3 | type: "component",
4 | description: "Provides tweaks for the Data (\"Variables\") block category.",
5 | credits: [
6 | {
7 | name: "GarboMuffin@ScratchAddons",
8 | link: "https://github.com/GarboMuffin",
9 | },
10 | {
11 | name: "fath11@Cocrea",
12 | link: "https://cocrea.world/@Fath11",
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/CollapsibleItemView/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import classNames from "classnames";
3 | import RightIcon from "assets/icon--down.svg";
4 | import styles from "./styles.less";
5 |
6 | export interface TargetItemViewProps {
7 | collapsible?: boolean;
8 | name?: string;
9 | variables: {
10 | variables?: string[];
11 | lists?: string[];
12 | };
13 | }
14 |
15 | const CollapsibleItemView: React.FC<{
16 | collapsed: boolean;
17 | name: string;
18 | onClick: () => void;
19 | }> = ({ name, collapsed, onClick }) => (
20 |
21 |
22 |
23 |
24 | {name}
25 |
26 | );
27 |
28 | export default CollapsibleItemView;
29 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/CollapsibleItemView/styles.less:
--------------------------------------------------------------------------------
1 | .row {
2 | position: relative;
3 | display: flex;
4 | align-items: center;
5 | white-space: nowrap;
6 | }
7 |
8 | .row:hover {
9 | background-color: var(--theme-color-50);
10 | }
11 |
12 | .collapse-icon {
13 | width: 12px;
14 | height: 12px;
15 | margin-right: 4px;
16 | position: relative;
17 | }
18 |
19 | .collapse-icon svg {
20 | position: absolute;
21 | }
22 |
23 | .collapsed .collapse-icon svg {
24 | transform: rotate(-90deg);
25 | }
26 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/Content/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { defineMessages } from "@formatjs/intl";
3 | import { useDevToolsContext } from "src/plugins/dev-tools/lib/context";
4 | import Tab from "components/Tab";
5 | import IF from "components/IF";
6 | import VariablesView from "../VariablesView";
7 | import TackedVariables from "../TackedVariables";
8 |
9 | import styles from "./styles.less";
10 |
11 | const messages = defineMessages({
12 | variables: {
13 | id: "plugins.devTools.variables",
14 | defaultMessage: "Variables",
15 | description: "Show all the variables in the project",
16 | },
17 | variableMonitor: {
18 | id: "plugins.devTools.variableMonitor",
19 | defaultMessage: "VariableMonitor",
20 | description: "All variables specified for observation",
21 | },
22 | });
23 |
24 | const DevToolsPluginContent: React.FC = () => {
25 | const { intl } = useDevToolsContext();
26 | const [currentIndex, setCurrentIndex] = React.useState(0);
27 |
28 | React.useEffect(() => {
29 | window.$monitoringVariable = true;
30 | return () => {
31 | window.$monitoringVariable = false;
32 | };
33 | }, []);
34 |
35 | return (
36 |
37 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | DevToolsPluginContent.displayName = "DevToolsPluginContent";
54 |
55 | export default DevToolsPluginContent;
56 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/Content/styles.less:
--------------------------------------------------------------------------------
1 | .container {
2 | position: relative;
3 | border-radius: 0 0 8px 8px;
4 | font-size: 12px;
5 | line-height: 18px;
6 | padding: 9px 0;
7 | color: var(--theme-color-g300);
8 | height: calc(100% - 25px);
9 | }
10 |
11 | .container ul {
12 | list-style: none;
13 | position: relative;
14 | /* 这里必须设置背景色,用来遮盖住 未添加变量 的提示 */
15 | background: var(--theme-color-300);
16 | }
17 |
18 | .container ul+li,
19 | .container li+li {
20 | margin-top: 6px;
21 | }
22 |
23 | .container svg+span {
24 | margin-left: 4px;
25 | }
26 |
27 | .tab-wrapper {
28 | height: calc(100% - 15px);
29 | overflow: auto;
30 | }
31 |
32 | .tab {
33 | margin: 0 9px;
34 | }
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/Entrance/styles.less:
--------------------------------------------------------------------------------
1 | .dev-tools {
2 | position: relative;
3 | }
4 |
5 | .search-icon {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | width: 32px;
10 | height: 32px;
11 | border-radius: 4px;
12 | cursor: pointer;
13 | }
14 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/ListView/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Bubble from "components/Bubble";
3 | import { useVirtualizer } from "@tanstack/react-virtual";
4 |
5 | import type { ListVariable } from "../../lib/dev-tools-observer";
6 | import styles from "./styles.less";
7 |
8 | export interface ListViewProps {
9 | value: ListVariable;
10 | }
11 |
12 | const ListView: React.FC = ({ value }) => {
13 | const parentRef = React.useRef();
14 |
15 | // The virtualizer
16 | const rowVirtualizer = useVirtualizer({
17 | count: (value || []).length,
18 | getScrollElement: () => parentRef.current,
19 | estimateSize: () => 24,
20 | });
21 |
22 | return (
23 |
24 | {/* The large inner element to hold all of the items */}
25 |
31 | {/* Only the visible items in the virtualizer, manually positioned to be in view */}
32 | {rowVirtualizer.getVirtualItems().map((virtualItem) => (
33 |
41 | {virtualItem.index + 1}
42 |
43 | {value[virtualItem.index]}
44 |
45 |
46 | ))}
47 |
48 |
49 | );
50 | };
51 |
52 | ListView.displayName = "ListView";
53 |
54 | export default ListView;
55 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/ListView/styles.less:
--------------------------------------------------------------------------------
1 | .virtual-list-container {
2 | width: 100%;
3 | max-height: 240px;
4 | overflow: auto;
5 | }
6 |
7 | .virtual-list-container-inner {
8 | width: 100%;
9 | position: relative;
10 | }
11 |
12 | .virtual-list-item {
13 | position: absolute;
14 | left: 0;
15 | top: 0;
16 | width: 100%;
17 | padding-top: 6px;
18 | padding-left: 54px;
19 | overflow: hidden;
20 | text-overflow: ellipsis;
21 | white-space: nowrap;
22 | }
23 |
24 | .index {
25 | display: inline-block;
26 | padding: 0 2px;
27 | color: var(--theme-color-400);
28 | background: var(--theme-color-g400);
29 | border-radius: 14px;
30 | margin-right: 2px;
31 | min-width: 14px;
32 | line-height: 14px;
33 | text-align: center;
34 | }
35 |
36 | .text {
37 | color: var(--theme-color-g300);
38 | }
39 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/TackedVariableView/styles.less:
--------------------------------------------------------------------------------
1 | .row > li {
2 | padding: 3px 9px;
3 | }
4 |
5 | :global(.react-draggable-dragging).row {
6 | position: relative;
7 | z-index: 1;
8 | }
9 |
10 | .clone-list {
11 | padding-top: 6px;
12 | }
13 |
14 | .draggable-icon {
15 | position: relative;
16 | }
17 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/TackedVariables/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { defineMessages } from "react-intl";
3 | import { useDevToolsContext } from "src/plugins/dev-tools/lib/context";
4 | import { devToolsObserver } from "src/plugins/dev-tools/lib/dev-tools-observer";
5 | import { enableLegendStateReact, observer } from "@legendapp/state/react";
6 | import { DraggableData } from "react-draggable";
7 | import TackedVariableView from "../TackedVariableView";
8 |
9 | import styles from "./styles.less";
10 |
11 | enableLegendStateReact();
12 |
13 | const messages = defineMessages({
14 | tackedVariablesEmpty: {
15 | id: "plugins.devTools.tackedVariablesEmpty",
16 | defaultMessage: "No monitoring variable added.",
17 | description: "No monitoring variable added. \n Click the 'pin' button in front of the variable to monitor it.",
18 | },
19 | });
20 |
21 | const TackedVariables: React.FC = observer(() => {
22 | const { intl } = useDevToolsContext();
23 | const [variableIdList, setVariableIdList] = React.useState([]);
24 | const variableIdListRef = React.useRef(variableIdList);
25 |
26 | const handleDrag = React.useCallback(
27 | (index: number, ui: DraggableData) => {
28 | const draggedItem = variableIdListRef.current[index];
29 | const newList = [...variableIdListRef.current];
30 | newList.splice(Number(index), 1);
31 | newList.splice(ui.y / 24 + index, 0, draggedItem);
32 | devToolsObserver.tackedVariables.set(
33 | newList.reduce(
34 | (acc, v, index) => ({
35 | ...acc,
36 | [v.key]: {
37 | ...v,
38 | index,
39 | },
40 | }),
41 | {},
42 | ),
43 | );
44 | },
45 | [variableIdList],
46 | );
47 |
48 | const handleTackedVariables = React.useCallback(() => {
49 | const tackedVariables = devToolsObserver.tackedVariables.peek();
50 | const newList = Object.keys(tackedVariables).map((key) => ({
51 | key,
52 | ...tackedVariables[key],
53 | }));
54 | newList.sort((a, b) => a.index - b.index);
55 | setVariableIdList(newList);
56 | }, []);
57 |
58 | React.useEffect(() => {
59 | handleTackedVariables();
60 | const dispose = devToolsObserver.tackedVariables.onChange(() => {
61 | handleTackedVariables();
62 | });
63 | return () => {
64 | dispose();
65 | };
66 | }, []);
67 |
68 | React.useEffect(() => {
69 | variableIdListRef.current = [...variableIdList];
70 | }, [variableIdList]);
71 |
72 | return (
73 |
74 | {variableIdList.map((item, index) => (
75 |
76 | ))}
77 | {!variableIdList.length && (
78 | {intl.formatMessage(messages.tackedVariablesEmpty)}
79 | )}
80 |
81 | );
82 | });
83 |
84 | TackedVariables.displayName = "TackedVariables";
85 |
86 | export default TackedVariables;
87 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/TackedVariables/styles.less:
--------------------------------------------------------------------------------
1 | .container {
2 | font-size: 12px;
3 | line-height: 18px;
4 | color: var(--theme-color-g500);
5 | height: 100%;
6 | padding: 9px 0;
7 | border-radius: 0 0 8px 8px;
8 | }
9 |
10 | .tacked-variables-empty {
11 | white-space: pre-wrap;
12 | text-align: center;
13 | position: relative;
14 | top: 50%;
15 | left: 50%;
16 | transform: translate(-50%, -50%);
17 | padding: 0 9px;
18 | }
19 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/TargetView/styles.less:
--------------------------------------------------------------------------------
1 | .retract {
2 | padding-left: 16px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/VariableView/styles.less:
--------------------------------------------------------------------------------
1 | .row {
2 | position: relative;
3 | display: flex;
4 | align-items: center;
5 | white-space: nowrap;
6 | }
7 |
8 | .clickable {
9 | cursor: pointer;
10 | }
11 |
12 | .row:hover {
13 | background-color: var(--theme-color-50);
14 | }
15 |
16 | .indent {
17 | padding-left: 16px;
18 | }
19 |
20 | .double-indent {
21 | padding-left: 32px;
22 | }
23 |
24 | .variable-name {
25 | margin-right: 0.5em;
26 | flex-shrink: 0;
27 | max-width: 50%;
28 | }
29 |
30 | .variable-value {
31 | color: var(--theme-color-g300);
32 | }
33 |
34 | .ellipsis {
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | white-space: nowrap;
38 | }
39 |
40 | .collapse-icon {
41 | width: 12px;
42 | height: 12px;
43 | margin-right: 4px;
44 | position: relative;
45 | flex-shrink: 0;
46 | }
47 |
48 | .collapse-icon svg {
49 | position: absolute;
50 | }
51 |
52 | .collapsed .collapse-icon svg {
53 | transform: rotate(-90deg);
54 | }
55 |
56 | .tackIcon {
57 | margin-right: 6px;
58 | }
59 |
60 | .icon-list {
61 | height: 16px;
62 | flex: 1;
63 | text-align: right;
64 | }
65 |
66 | .icon {
67 | display: inline-block;
68 | width: 16px;
69 | height: 16px;
70 | border-radius: 4px;
71 | cursor: pointer;
72 | }
73 |
74 | .icon:hover {
75 | background-color: var(--theme-color-50);
76 | }
77 |
78 | .icon + .icon {
79 | margin-left: 12px;
80 | }
81 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/components/VariablesView/styles.less:
--------------------------------------------------------------------------------
1 | .variables {
2 | padding: 0 9px;
3 | max-height: calc(100% - 16px);
4 | overflow: auto;
5 | }
6 |
7 | .category {
8 | min-height: 68px;
9 | padding: 12px 0;
10 | position: relative;
11 | }
12 |
13 | .category ~ .category {
14 | border-top: 1px solid var(--theme-color-50);
15 | }
16 |
17 | .category-name {
18 | margin-bottom: 8px;
19 | font-weight: 500;
20 | }
21 |
22 | .empty {
23 | position: absolute;
24 | width: 100%;
25 | text-align: center;
26 | color: var(--theme-color-g500);
27 | }
28 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import DevToolsPluginEntrance from "./components/Entrance";
3 | import DevToolsPluginContent from "./components/Content";
4 | import { DevToolsContext } from "./lib/context";
5 |
6 | const DevTools: React.FC = (props) => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | DevTools.displayName = "DevToolsPlugin";
17 |
18 | export default DevTools;
19 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/lib/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 |
3 | export const DevToolsContext = createContext(null);
4 |
5 | export const useDevToolsContext = () => useContext(DevToolsContext);
6 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/lib/dev-tools-observer.ts:
--------------------------------------------------------------------------------
1 | import { observable } from "@legendapp/state";
2 | import { VariableItemViewProps } from "../components/VariableView";
3 | import EventBus from "./event-bus";
4 |
5 | export type ListVariable = Array;
6 |
7 | export const devToolsObserver = observable<{
8 | tackedVariables: Record;
9 | targetAndNameMap: Record;
10 | }>({ tackedVariables: {}, targetAndNameMap: {} });
11 |
12 | export const variableKeysList = new Set();
13 |
14 | export const variableChangeEventBus = new EventBus();
15 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/lib/event-bus.ts:
--------------------------------------------------------------------------------
1 | import { debounce } from "lodash-es";
2 |
3 | export type EventCallback = (...args: unknown[]) => void;
4 |
5 | type Listener = {
6 | callback: EventCallback;
7 | };
8 |
9 | class EventBus {
10 | private listeners: { [key: string]: Listener[] } = {};
11 |
12 | // creates an event that can be triggered any number of times
13 | on(eventName: string, callback: EventCallback) {
14 | this.registerListener(eventName, callback);
15 | }
16 |
17 | // kill an event with all it's callbacks
18 | off(eventName: string) {
19 | delete this.listeners[eventName];
20 | }
21 |
22 | // removes the given callback for the given event
23 | detach(eventName: string, callback: EventCallback) {
24 | let listeners = this.listeners[eventName] || [];
25 |
26 | listeners = listeners.filter(function (value) {
27 | return value.callback !== callback;
28 | });
29 |
30 | if (eventName in this.listeners) {
31 | this.listeners[eventName] = listeners;
32 | }
33 | }
34 |
35 | // removes all the events for the given name
36 | detachAll(eventName: string) {
37 | this.off(eventName);
38 | }
39 |
40 | emit(eventName: string, ...args: unknown[]) {
41 | let listeners: Listener[] = [];
42 |
43 | // name exact match
44 | if (this.hasListeners(eventName)) {
45 | listeners = this.listeners[eventName];
46 | }
47 |
48 | listeners.forEach((listener) => {
49 | const callback = listener.callback;
50 | callback(...args);
51 | });
52 | }
53 |
54 | hasListeners(eventName: string): boolean {
55 | return eventName in this.listeners;
56 | }
57 |
58 | private registerListener(eventName: string, callback: EventCallback) {
59 | if (!this.hasListeners(eventName)) {
60 | this.listeners[eventName] = [];
61 | }
62 |
63 | this.listeners[eventName].push({
64 | callback: debounce(callback, 100, {
65 | maxWait: 300,
66 | }),
67 | });
68 | }
69 | }
70 |
71 | export default EventBus;
72 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/lib/proxy-variable.ts:
--------------------------------------------------------------------------------
1 | import { variableChangeEventBus } from "./dev-tools-observer";
2 |
3 | export interface VariableChangeEventDetail {
4 | propertyName: string;
5 | value: Scratch.Variable["value"];
6 | }
7 |
8 | const onChange = (eventName: string, propertyName: string, value: Scratch.Variable["value"]) => {
9 | variableChangeEventBus.emit(eventName, {
10 | propertyName,
11 | value,
12 | });
13 | };
14 |
15 | function proxyVariableList(value: Array, eventName: string) {
16 | return new Proxy(value, {
17 | set(list, idx, val) {
18 | list[idx] = val;
19 | if (idx !== "length") {
20 | onChange(eventName, "value", [...list]);
21 | }
22 | return true;
23 | },
24 | });
25 | }
26 |
27 | export function addProxy(variable: Scratch.Variable) {
28 | const eventName = `${variable.targetId}${variable.id}`;
29 | const type = variable.type;
30 | let value =
31 | type === "list" ? proxyVariableList(variable.value as (string | number | boolean)[], eventName) : variable.value;
32 | let name = variable.name;
33 | Object.defineProperties(variable, {
34 | value: {
35 | get() {
36 | return value;
37 | },
38 | set: function (newValue) {
39 | const oldValue = value;
40 | if (type === "list") {
41 | newValue = proxyVariableList(newValue, eventName);
42 | }
43 | value = newValue;
44 | if (oldValue !== newValue) {
45 | onChange(eventName, "value", newValue);
46 | }
47 | },
48 | },
49 | name: {
50 | get() {
51 | return name;
52 | },
53 | set(newName: string) {
54 | const oldName = name;
55 | name = newName;
56 | if (oldName !== newName) {
57 | onChange(eventName, "name", name);
58 | }
59 | },
60 | },
61 | });
62 | }
63 |
64 | export function removeProxy(variable: Scratch.Variable, target: Scratch.RenderTarget) {
65 | const { id, name, type, value, isCloud, targetId } = variable;
66 | const Variable = variable.constructor;
67 | target.variables[id] = new Variable(id, name, type, isCloud, targetId);
68 | target.variables[id].value = type === "list" ? [...value] : value;
69 | }
70 |
--------------------------------------------------------------------------------
/src/plugins/dev-tools/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "DevTools",
3 | type: "component",
4 | description: "A series of debugging functions。",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/dropdown-searchable/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import DropdownSearchableIcon from "assets/icon--dropdown-searchable.svg";
3 |
4 | const DropdownSearchable: React.FC = ({ blockly, msg, registerSettings }) => {
5 | React.useEffect(() => {
6 | blockly.showDropdownSearchableDataType = false;
7 | blockly.showDropdownSearchableDropdowns = false;
8 | const register = registerSettings(
9 | msg("plugins.dropdownSearchable.title"),
10 | "plugin-dropdown-searchable",
11 | [
12 | {
13 | key: "dropdownSearchable",
14 | label: msg("plugins.dropdownSearchable.title"),
15 | description: msg("plugins.dropdownSearchable.description"),
16 | items: [
17 | {
18 | key: "dropdown",
19 | label: msg("plugins.dropdownSearchable.option.dropdown"),
20 | type: "switch",
21 | value: false,
22 | onChange: (value: boolean) => {
23 | blockly.showDropdownSearchableDropdowns = value;
24 | },
25 | },
26 | {
27 | key: "input",
28 | label: msg("plugins.dropdownSearchable.option.input"),
29 | type: "switch",
30 | value: false,
31 | onChange: (value: boolean) => {
32 | blockly.showDropdownSearchableDataType = value;
33 | },
34 | },
35 | ],
36 | },
37 | ],
38 | ,
39 | );
40 | return () => {
41 | register.dispose();
42 | };
43 | }, [registerSettings, msg]);
44 |
45 | return null;
46 | };
47 |
48 | DropdownSearchable.displayName = "DropdownSearchable";
49 |
50 | export default DropdownSearchable;
51 |
--------------------------------------------------------------------------------
/src/plugins/dropdown-searchable/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "dropdown-searchable",
3 | type: "component",
4 | description: "Support dropdown menu search and data-type block search.",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/extension-manager/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Extension Manager",
3 | type: "component",
4 | description: "An extension manager to make extensions, less annoying, for YOU.",
5 | credits: [
6 | {
7 | name: "fath11@Cocrea",
8 | link: "https://cocrea.world/@Fath11",
9 | },
10 | {
11 | name: "Cubester@NitroBolt",
12 | link: "https://github.com/CubesterYT",
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/extension-manager/styles.less:
--------------------------------------------------------------------------------
1 | :global(.addons_tip-icon_oy8QS).extensionManagerTooltip {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 32px;
6 | height: 32px;
7 | border-radius: 4px;
8 | cursor: pointer;
9 | }
10 |
11 | .extensionManager-item-shadow {
12 | display: flex;
13 | align-items: center;
14 | width: 100%;
15 | height: 48px;
16 | position: relative;
17 | font-size: 14px;
18 | color: transparent;
19 | background-color: var(--gui-item-hover);
20 | -webkit-user-select: none;
21 | user-select: none;
22 | border-bottom: 1px solid var(--gui-divider-color);
23 | padding: 3px;
24 | border-radius: 5px;
25 | cursor: grab;
26 | }
27 |
28 | .extensionManager-item {
29 | display: flex;
30 | align-items: center;
31 | width: 100%;
32 | height: 48px;
33 | position: relative;
34 | font-size: 14px;
35 | color: var(--theme-text-primary);
36 | -webkit-user-select: none;
37 | user-select: none;
38 | border-bottom: 1px solid var(--gui-divider-color);
39 | padding: 3px;
40 | border-radius: 5px;
41 | cursor: grab;
42 | }
43 |
44 | .extensionManager-item_info {
45 | flex: auto;
46 | font-size: 14px;
47 | -webkit-user-select: none;
48 | user-select: none;
49 | }
50 |
51 | .extensionManager-item_delete {
52 | background-color: transparent;
53 | border-radius: 5px;
54 | padding: 3px;
55 | border-style: none;
56 | cursor: pointer;
57 | }
58 |
59 | .extensionManager-item {
60 | .extensionManager-item_delete {
61 | display: flex;
62 | }
63 | }
64 |
65 | .extensionManager-item.lift {
66 | .extensionManager-item_delete {
67 | display: none;
68 | }
69 |
70 | &:hover {
71 | .extensionManager-item_delete {
72 | display: flex;
73 | }
74 | }
75 | }
76 |
77 | .extensionManager-item_delete:hover {
78 | background-color: var(--theme-error-color-p3);
79 | }
80 |
81 | .extensionManager-item_notSelected {
82 | background-color: transparent;
83 | border-radius: 5px;
84 | padding: 3px;
85 | border-style: none;
86 | cursor: pointer;
87 | }
88 |
89 | .extensionManager-item_selected {
90 | background-color: transparent;
91 | border-radius: 5px;
92 | padding: 3px;
93 | border-style: none;
94 | cursor: pointer;
95 | }
96 |
97 | .extensionManager-item_notSelected, .extensionManager-item_selected {
98 | background-color: transparent;
99 | border-radius: 5px;
100 | padding: 3px;
101 | display: flex;
102 | }
103 |
104 | .extensionManager-item_selected svg rect {
105 | fill: var(--gui-item-active);
106 | stroke: var(--gui-item-active);
107 | }
108 |
109 | .extensionManager_item-container {
110 | padding: 10px;
111 | overflow-y: auto;
112 | overflow-x: hidden;
113 | }
114 |
115 | .lift {
116 | box-shadow: 0 0 10px 0px var(--gui-item-active);
117 | }
118 |
119 | .shakeAnimation {
120 | animation: shakeKeyframes 0.5s linear;
121 | }
122 |
123 | @keyframes shakeKeyframes {
124 | 0% {
125 | transform: translate(1px, 1px) rotate(0deg);
126 | }
127 | 10% {
128 | transform: translate(-1px, -2px) rotate(-0.5deg);
129 | }
130 | 20% {
131 | transform: translate(-3px, 0px) rotate(0.5deg);
132 | }
133 | 30% {
134 | transform: translate(3px, 2px) rotate(0deg);
135 | }
136 | 40% {
137 | transform: translate(1px, -1px) rotate(0.5deg);
138 | }
139 | 50% {
140 | transform: translate(-1px, 2px) rotate(-0.5deg);
141 | }
142 | 60% {
143 | transform: translate(-3px, 1px) rotate(0deg);
144 | }
145 | 70% {
146 | transform: translate(3px, 1px) rotate(-0.5deg);
147 | }
148 | 80% {
149 | transform: translate(-1px, -1px) rotate(0.5deg);
150 | }
151 | 90% {
152 | transform: translate(1px, 2px) rotate(0deg);
153 | }
154 | 100% {
155 | transform: translate(1px, -2px) rotate(-0.5deg);
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/plugins/fast-input/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "fast-input",
3 | type: "function",
4 | description: "Quickly insert blocks",
5 | credits: [
6 | {
7 | name: "Tacodiva@ScratchAddons",
8 | link: "https://github.com/Tacodiva",
9 | },
10 | {
11 | name: "griffpatch@ScratchAddons",
12 | },
13 | {
14 | name: "TheColaber@ScratchAddons",
15 | link: "https://scratch.mit.edu/users/TheColaber/",
16 | },
17 | {
18 | name: "CST1229@ScratchAddons",
19 | link: "https://github.com/CST1229",
20 | },
21 | {
22 | name: "Cappu",
23 | link: "https://www.ccw.site/student/62b963fe01fb2c3c96d5d8ad",
24 | },
25 | ],
26 | };
27 |
--------------------------------------------------------------------------------
/src/plugins/fast-input/module.js:
--------------------------------------------------------------------------------
1 | const textWidthCache = new Map();
2 | const textWidthCacheSize = 1000;
3 |
4 | const eventTarget = new EventTarget();
5 | const eventClearTextCache = "clearTextCache";
6 |
7 | /**
8 | * Gets the width of an svg text element, with caching.
9 | * @param {SVGTextElement} textElement
10 | */
11 | export function getTextWidth(textElement) {
12 | let string = textElement.innerHTML;
13 | if (string.length === 0) return 0;
14 | let width = textWidthCache.get(string);
15 | if (width) return width;
16 | width = textElement.getBoundingClientRect().width;
17 | textWidthCache.set(string, width);
18 | if (textWidthCache.size > textWidthCacheSize) {
19 | textWidthCache.delete(textWidthCache.keys().next());
20 | }
21 | return width;
22 | }
23 |
24 | /**
25 | * Clears the text width cache of the middle click popup.
26 | */
27 | export function clearTextWidthCache() {
28 | textWidthCache.clear();
29 | eventTarget.dispatchEvent(new CustomEvent(eventClearTextCache));
30 | }
31 |
32 | /**
33 | * @param {() => void} func
34 | */
35 | export function onClearTextWidthCache(func) {
36 | eventTarget.addEventListener(eventClearTextCache, func);
37 | }
38 |
--------------------------------------------------------------------------------
/src/plugins/fast-input/styles.css:
--------------------------------------------------------------------------------
1 | .sa-mcp-root {
2 | display: flex;
3 | white-space: nowrap;
4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
5 | position: absolute;
6 | min-width: 100px;
7 | border-radius: 8px;
8 | box-shadow: 0px 24px 20px rgba(32, 57, 94, 0.05), 0px 2px 20px rgba(0, 0, 0, 0.08);
9 | z-index: 101;
10 | }
11 |
12 | .sa-mcp-container {
13 | display: flex;
14 | flex-flow: column;
15 | position: absolute;
16 | top: -6px;
17 | box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.3);
18 | border-radius: 8px;
19 | background-color: var(--theme-color-200);
20 | overflow: hidden;
21 | }
22 |
23 | .sa-mcp-input-wrapper {
24 | position: relative;
25 | margin: 5px 8px;
26 | height: 32px;
27 | line-height: 32px;
28 | background: var(--theme-color-500);
29 | border: 1px solid var(--theme-color-200);
30 | border-radius: 8px;
31 | }
32 |
33 | .sa-mcp-input-wrapper:focus-within {
34 | border: 1px solid var(--theme-brand-color);
35 | }
36 |
37 | .sa-mcp-input-wrapper[data-error="true"] {
38 | border-color: red;
39 | }
40 |
41 | .sa-mcp-input-wrapper .sa-mcp-input {
42 | color: var(--theme-text-primary);
43 | }
44 |
45 | .sa-mcp-input-wrapper > input {
46 | position: absolute;
47 | outline: none;
48 | border: 0;
49 | width: 100%;
50 | font-size: 12px;
51 | padding: 7px 8px;
52 | background-color: transparent;
53 | border-radius: 8px;
54 | overflow: hidden;
55 | text-overflow: ellipsis;
56 | white-space: nowrap;
57 | }
58 |
59 | .sa-mcp-input-suggestion {
60 | color: hsla(225, 15%, 40%, 0.65);
61 | }
62 |
63 | .sa-mcp-preview-container {
64 | padding: 0 4px;
65 | flex: auto;
66 | overflow-y: scroll;
67 | scrollbar-width: none;
68 | }
69 |
70 | .sa-mcp-preview-container::-webkit-scrollbar {
71 | width: 0;
72 | height: 0;
73 | }
74 |
75 | .sa-mcp-preview-blocks {
76 | width: 100%;
77 | min-height: 100%;
78 | /* https://stackoverflow.com/a/22166728/8448397 */
79 | float: left;
80 | }
81 |
82 | .sa-mcp-preview-scrollbar {
83 | position: absolute;
84 | width: 11px;
85 | right: 0;
86 | bottom: 0;
87 | }
88 |
89 | .sa-mcp-preview-block-bg {
90 | width: 100%;
91 | fill: transparent;
92 | cursor: grab;
93 | }
94 |
95 | .sa-mcp-preview-block {
96 | filter: brightness(95%);
97 | cursor: grab;
98 | }
99 |
100 | .sa-mcp-preview-block-selection {
101 | filter: brightness(103%);
102 | }
103 |
104 | .sa-mcp-preview-block-bg-selection {
105 | fill: #7774;
106 | }
107 |
--------------------------------------------------------------------------------
/src/plugins/folder/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const Folder: React.FC = ({ redux }) => {
4 | React.useEffect(() => {
5 | redux.dispatch({
6 | type: "scratch-gui/global-settings/UPDATE_FOLDER_USEABLE",
7 | useable: true,
8 | });
9 | return () => {
10 | redux.dispatch({
11 | type: "scratch-gui/global-settings/UPDATE_FOLDER_USEABLE",
12 | useable: false,
13 | });
14 | };
15 | }, [redux]);
16 |
17 | return null;
18 | };
19 |
20 | Folder.displayName = "Folder";
21 |
22 | export default Folder;
23 |
--------------------------------------------------------------------------------
/src/plugins/folder/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "folder",
3 | type: "component",
4 | description: "Adds folders to the sprite pane, as well as costume and sound lists.",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/historical-version/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import HistoricalVersionIcon from "assets/icon--statistics.svg";
3 |
4 | const HistoricalVersion: React.FC = ({ redux, msg, registerSettings }) => {
5 | React.useEffect(() => {
6 | redux.dispatch({
7 | type: "scratch-gui/global-settings/SET_HISTORICAL_VERSION_USEABLE",
8 | visible: true,
9 | });
10 | const register = registerSettings(
11 | msg("plugins.historicalVersion.title"),
12 | "plugin-historical-version",
13 | [
14 | {
15 | key: "local",
16 | label: msg("plugins.historicalVersion.title"),
17 | items: [
18 | {
19 | key: "autoSaveable",
20 | type: "switch",
21 | label: msg("plugins.historicalVersion.autoSaveable"),
22 | value: true,
23 | onChange: (value) => {
24 | redux.dispatch({
25 | type: "scratch-gui/global-settings/SET_HISTORICAL_VERSION_AUTO_SAVEABLE",
26 | value,
27 | });
28 | },
29 | },
30 | {
31 | key: "maximum",
32 | type: "input",
33 | inputProps: {
34 | type: "number",
35 | },
36 | label: msg("plugins.historicalVersion.maximum"),
37 | value: 100,
38 | onChange: (value) => {
39 | redux.dispatch({
40 | type: "scratch-gui/global-settings/SET_HISTORICAL_VERSION_AUTO_SAVEABLE_MAXIMUM",
41 | value: Number.isNaN(Number(value)) ? 0 : Number(value),
42 | });
43 | },
44 | },
45 | {
46 | key: "interval",
47 | type: "select",
48 | label: msg("plugins.historicalVersion.interval"),
49 | value: 5 * 60 * 1000,
50 | options: [
51 | { label: `5 ${msg("plugins.historicalVersion.mins")}`, value: 5 * 60 * 1000 },
52 | { label: `10 ${msg("plugins.historicalVersion.mins")}`, value: 10 * 60 * 1000 },
53 | { label: `30 ${msg("plugins.historicalVersion.mins")}`, value: 30 * 60 * 1000 },
54 | { label: `60 ${msg("plugins.historicalVersion.mins")}`, value: 60 * 60 * 1000 },
55 | ],
56 | onChange: (value) => {
57 | redux.dispatch({
58 | type: "scratch-gui/global-settings/SET_HISTORICAL_VERSION_AUTO_SAVEABLE_INTERVAL_SECS",
59 | value,
60 | });
61 | },
62 | },
63 | ],
64 | },
65 | ],
66 | ,
67 | );
68 | return () => {
69 | redux.dispatch({
70 | type: "scratch-gui/global-settings/SET_HISTORICAL_VERSION_USEABLE",
71 | visible: false,
72 | });
73 | register.dispose();
74 | };
75 | }, [registerSettings, msg]);
76 |
77 | return null;
78 | };
79 |
80 | HistoricalVersion.displayName = "HistoricalVersion";
81 |
82 | export default HistoricalVersion;
83 |
--------------------------------------------------------------------------------
/src/plugins/historical-version/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "historical-version",
3 | type: "component",
4 | description: "Save a copy of your current work locally on a regular basis.",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/inspiro/componet/Contact.ts:
--------------------------------------------------------------------------------
1 | export interface Contact {
2 | name: string;
3 | avatar: string;
4 | desc?: string;
5 | generator?: (text: string) => Promise;
6 | type?: Type;
7 | duration?: number;
8 | }
9 |
10 | export enum Type {
11 | MUSIC = "MUSIC",
12 | IMAGE = "IMAGE",
13 | }
14 |
--------------------------------------------------------------------------------
/src/plugins/inspiro/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styles from "./styles.less";
3 | import ReactDOM from "react-dom";
4 | import { defineMessage } from "@formatjs/intl";
5 | import Tooltip from "components/Tooltip";
6 | import InspiroIcon from "assets/icon--inspiro.svg";
7 | import ExpansionBox, { ExpansionRect } from "components/ExpansionBox";
8 | import Entrance from "./componet/Entrance";
9 | import useStorageInfo from "hooks/useStorageInfo";
10 |
11 | const messages = defineMessage({
12 | title: {
13 | id: "plugins.inspiro.title",
14 | defaultMessage: "AI 助手",
15 | },
16 | intro: {
17 | id: "plugins.inspiro.intro",
18 | defaultMessage:
19 | "您在 Gandi IDE 中的全能 AI 助手!无论您是想生成令人惊叹的图片、创作动听的音乐,还是需要灵感来实现您的创意梦想,创灵助手都能为您提供无尽的可能性。",
20 | },
21 | });
22 |
23 | const DEFAULT_CONTAINER_INFO = {
24 | width: 724,
25 | height: 543,
26 | translateX: 0,
27 | translateY: 0,
28 | };
29 |
30 | const Inspiro: React.FC = ({ intl, utils, vm, registerSettings, trackEvents }) => {
31 | const [visible, setVisible] = React.useState(false);
32 | const containerRef = React.useRef(null);
33 | const [containerInfo, setContainerInfo] = useStorageInfo(
34 | "DEFAULT_CHAT_CONTAINER_INFO",
35 | DEFAULT_CONTAINER_INFO,
36 | );
37 | const containerInfoRef = React.useRef(containerInfo);
38 | const getContainerPosition = React.useCallback(() => {
39 | const windowWidth = window.innerWidth;
40 | const windowHeight = window.innerHeight;
41 | const x = (windowWidth - containerInfoRef.current.width) / 2;
42 | const y = (windowHeight - containerInfoRef.current.height) / 2;
43 | return {
44 | translateX: x,
45 | translateY: y,
46 | };
47 | }, []);
48 | const handleShow = React.useCallback(() => {
49 | const windowWidth = window.innerWidth;
50 | const windowHeight = window.innerHeight;
51 |
52 | const aspectRatio = DEFAULT_CONTAINER_INFO.width / DEFAULT_CONTAINER_INFO.height;
53 | let newWidth = windowWidth * 0.8;
54 | let newHeight = newWidth / aspectRatio;
55 |
56 | if (newHeight > windowHeight * 0.8) {
57 | newHeight = windowHeight * 0.8;
58 | newWidth = newHeight * aspectRatio;
59 | }
60 | setContainerInfo({
61 | ...containerInfoRef.current,
62 | width: newWidth,
63 | height: newHeight,
64 | ...getContainerPosition(),
65 | });
66 | setVisible(true);
67 | }, []);
68 |
69 | const handleClose = () => {
70 | setVisible(false);
71 | };
72 | const handleSizeChange = React.useCallback((value: ExpansionRect) => {
73 | containerInfoRef.current = value;
74 | }, []);
75 | return ReactDOM.createPortal(
76 |
77 | }
80 | onClick={handleShow}
81 | tipText={intl.formatMessage(messages.title)}
82 | />
83 | {visible &&
84 | ReactDOM.createPortal(
85 |
95 |
96 | ,
97 | document.body,
98 | )}
99 | ,
100 | document.querySelector(".plugins-wrapper"),
101 | );
102 | };
103 |
104 | Inspiro.displayName = "Inspiro";
105 |
106 | export default Inspiro;
107 |
--------------------------------------------------------------------------------
/src/plugins/inspiro/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "inspiro",
3 | type: "component",
4 | description: "AI 助手",
5 | credits: [
6 | {
7 | name: "JaggerZhong",
8 | },
9 | {
10 | name: "L",
11 | },
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/src/plugins/inspiro/styles.less:
--------------------------------------------------------------------------------
1 | .inspiro-root {
2 | position: relative;
3 | }
4 |
5 | .icon {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | width: 32px;
10 | height: 32px;
11 | border-radius: 4px;
12 | cursor: pointer;
13 | }
14 |
15 | .scaling-button {
16 | transition: transform 0.3s ease;
17 | padding: 0.1em 0.2em;
18 | }
19 |
20 | .scaling-button:hover {
21 | transform: scale(1.2);
22 | }
--------------------------------------------------------------------------------
/src/plugins/kukemc-beautify/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import KukemcBeautifyIcon from "assets/icon--kukemcbeautify.svg";
3 | import styles from "./styles.less";
4 |
5 | const KukemcBeautify: React.FC = ({ msg, registerSettings }) => {
6 | React.useEffect(() => {
7 | const register = registerSettings(
8 | msg("plugins.kukemcBeautify.title"),
9 | "plugin-kukemc-beautify",
10 | [
11 | {
12 | key: "kukemcBeautify",
13 | label: msg("plugins.kukemcBeautify.title"),
14 | description: msg("plugins.kukemcBeautify.description"),
15 | items: [
16 | {
17 | key: "Ground",
18 | label: msg("plugins.kukemcBeautify.frostedGlass"),
19 | type: "switch",
20 | value: false,
21 | onChange: (value: boolean) => {
22 | if (value) {
23 | document.body.classList.add(styles.frostedGlass);
24 | } else {
25 | document.body.classList.remove(styles.frostedGlass);
26 | }
27 | },
28 | },
29 | {
30 | key: "transparent",
31 | label: msg("plugins.kukemcBeautify.transparency"),
32 | type: "input",
33 | inputProps: {
34 | type: "number",
35 | },
36 | value: 0.29,
37 | onChange: (value: string) => {
38 | document.body.style.setProperty("--alpha", value);
39 | },
40 | },
41 | {
42 | key: "ambiguity",
43 | label: msg("plugins.kukemcBeautify.ambiguity"),
44 | type: "input",
45 | inputProps: {
46 | type: "number",
47 | },
48 | value: 10,
49 | onChange: (value: string) => {
50 | document.body.style.setProperty("--radius", (Number(value) || 0) + "px");
51 | },
52 | },
53 | ],
54 | },
55 | ],
56 | ,
57 | );
58 | return () => {
59 | document.body.classList.remove(styles.frostedGlass);
60 | register.dispose();
61 | };
62 | }, [registerSettings, msg]);
63 |
64 | return null;
65 | };
66 |
67 | KukemcBeautify.displayName = "KukemcBeautify";
68 |
69 | export default KukemcBeautify;
70 |
--------------------------------------------------------------------------------
/src/plugins/kukemc-beautify/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "kukemc-beautify",
3 | type: "component",
4 | description: "kukemc Gandi beautification",
5 | credits: [
6 | {
7 | name: "酷可mc",
8 | link: "https://www.ccw.site/student/610b508176415b2f27e0f851",
9 | },
10 | {
11 | name: "白猫@CCW",
12 | link: "https://www.ccw.site/student/6173f57f48cf8f4796fc860e",
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/kukemc-beautify/styles.less:
--------------------------------------------------------------------------------
1 | .frosted-glass section[class^="gandi_collapsible-box_collapsible-box"]::before {
2 | width: 100%;
3 | height: 100%;
4 | position: absolute;
5 | display: block;
6 | content: "";
7 | background-color: rgba(var(--bg-color), var(--alpha));
8 | backdrop-filter: blur(10px);
9 | border-radius: 8px;
10 | }
11 |
12 | .frosted-glass section[class^="gandi_collapsible-box_collapsible-box"] {
13 | background: transparent;
14 | }
15 |
16 | .frosted-glass header[class^="gandi_collapsible-box_header"] {
17 | border-color: var(--theme-color-200);
18 | }
19 |
20 | .frosted-glass div[class^="gandi_collapsible-box_body"] {
21 | position: relative;
22 | }
23 |
24 | :root[theme="dark"] .frosted-glass {
25 | --bg-color: 46, 54, 68;
26 | }
27 |
28 | :root[theme="light"] .frosted-glass {
29 | --bg-color: 247, 247, 247;
30 | }
31 |
32 | .frosted-glass {
33 | --alpha: 0.29;
34 | --radius: 10px;
35 |
36 | :global(.blocklyWidgetDiv .goog-menu),
37 | :global(.gandi_context-menu_context-menu_2SJM-),
38 | :global(.blocklyToolboxDiv),
39 | :global(.gandi_plugins_plugins-root_xA3t3),
40 | :global(.gandi_target-pane_count_3fmUd),
41 | :global(.gandi_editor-wrapper_tabList_4HFZz),
42 | :global(.gandi_setting-modal_modal-overlay_3wJji),
43 | :global(.addons_container_ML0OZ),
44 | :global(.gandi_expansion-box_container_3Bg1J),
45 | :global(.app-assets-center),
46 | :global(.layout-3Xaw0),
47 | :global(.addons_container_Gt4hs ul),
48 | :global(.gandi_bulletin-modal_modal-overlay_TBAhj) {
49 | background-color: rgba(var(--bg-color), var(--alpha));
50 | backdrop-filter: blur(var(--radius));
51 | }
52 | }
--------------------------------------------------------------------------------
/src/plugins/mobile-code-batch-select/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "mobile-code-batch-select",
3 | type: "component",
4 | description: "Helps you batch select code by your phone",
5 | credits: [
6 | {
7 | name: "白猫@CCW",
8 | link: "https://www.ccw.site/student/6173f57f48cf8f4796fc860e",
9 | },
10 | {
11 | name: "Luka@CCW",
12 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/mobile-code-batch-select/styles.less:
--------------------------------------------------------------------------------
1 | .code {
2 | line-height: 18px;
3 | color: #d1d5db;
4 | padding: 1px 6px;
5 | background: #3e495b;
6 | border-radius: 4px;
7 | margin: 0 2px;
8 | }
--------------------------------------------------------------------------------
/src/plugins/mobile-code-batch-select/touchZoom.ts:
--------------------------------------------------------------------------------
1 | const enableTouchZoom = (blockly: any, workspace: Blockly.WorkspaceSvg) => {
2 | let lastDist = 0;
3 | let isZooming = false;
4 |
5 | // 计算双指中心点
6 | const getTouchCenter = (touches: TouchList) => ({
7 | x: (touches[0].clientX + touches[1].clientX) / 2,
8 | y: (touches[0].clientY + touches[1].clientY) / 2,
9 | });
10 |
11 | // 计算双指距离
12 | const getTouchDistance = (touches: TouchList) => {
13 | const dx = touches[0].clientX - touches[1].clientX;
14 | const dy = touches[0].clientY - touches[1].clientY;
15 | return Math.sqrt(dx * dx + dy * dy);
16 | };
17 |
18 | // 记录上一次点击的时间和位置
19 | let lastTapTime = 0;
20 | let lastTapPos = { x: 0, y: 0 };
21 | let allowDrag = true;
22 |
23 | document.addEventListener(
24 | "touchstart",
25 | (event) => {
26 | const now = Date.now();
27 | const touch = event.changedTouches[0]; // 取第一个触摸点
28 | const tapPos = { x: touch.clientX, y: touch.clientY };
29 |
30 | // 检测是否是双击(时间间隔 < 300ms,且位置变化不大)
31 | const isDoubleTap =
32 | now - lastTapTime < 300 && Math.abs(tapPos.x - lastTapPos.x) < 20 && Math.abs(tapPos.y - lastTapPos.y) < 20;
33 |
34 | if (isDoubleTap) {
35 | allowDrag = false;
36 | return; // 直接返回,不执行 `originalStartDrag`
37 | }
38 |
39 | // 更新上一次点击的时间和位置
40 | lastTapTime = now;
41 | lastTapPos = tapPos;
42 | },
43 | { capture: true },
44 | );
45 |
46 | document.addEventListener(
47 | "touchend",
48 | () => {
49 | if (!allowDrag) {
50 | allowDrag = true;
51 | }
52 | },
53 | {
54 | capture: true,
55 | },
56 | );
57 |
58 | // 劫持 `startDrag`
59 | const originalStartDrag = blockly.WorkspaceDragger.prototype.startDrag;
60 | blockly.WorkspaceDragger.prototype.startDrag = function () {
61 | const event = window.event;
62 |
63 | if (event instanceof TouchEvent) {
64 | // 检测双指缩放
65 | if (event.touches.length === 2) {
66 | isZooming = true;
67 | lastDist = getTouchDistance(event.touches);
68 | }
69 | }
70 |
71 | return originalStartDrag.call(this);
72 | };
73 |
74 | // 劫持 `drag`
75 | const originalDrag = blockly.WorkspaceDragger.prototype.drag;
76 | blockly.WorkspaceDragger.prototype.drag = function (currentDragDeltaXY) {
77 | if (!allowDrag) {
78 | return;
79 | }
80 | if (!isZooming && window.event instanceof TouchEvent && window.event.touches.length === 2) {
81 | isZooming = true;
82 | }
83 | if (isZooming && window.event instanceof TouchEvent && window.event.touches.length === 2) {
84 | const newDist = getTouchDistance(window.event.touches);
85 | const newCenter = getTouchCenter(window.event.touches);
86 | const scaleSpeed = (newDist - lastDist) * 0.01 * 2; // 放大缩小的速度
87 |
88 | const position = blockly.utils.mouseToSvg(
89 | { clientX: newCenter.x, clientY: newCenter.y },
90 | workspace.getParentSvg(),
91 | workspace.getInverseScreenCTM(),
92 | );
93 | workspace.zoom(position.x, position.y, scaleSpeed);
94 | lastDist = newDist;
95 | return;
96 | //return originalDrag.call(this, getTouchCenterDistance());
97 | //因为scratch不能同时缩放和拖动,会导致(缩放中心点)坐标炸掉,所以这里不允许拖拽
98 | } else {
99 | return originalDrag.call(this, currentDragDeltaXY);
100 | }
101 | };
102 |
103 | // 劫持 `endDrag`
104 | const originalEndDrag = blockly.WorkspaceDragger.prototype.endDrag;
105 | blockly.WorkspaceDragger.prototype.endDrag = function (currentDragDeltaXY) {
106 | if (!allowDrag) {
107 | allowDrag = true;
108 | return;
109 | }
110 | if (isZooming) {
111 | isZooming = false;
112 | return;
113 | // return originalEndDrag.call(this, getTouchCenterDistance());
114 | } else {
115 | return originalEndDrag.call(this, currentDragDeltaXY);
116 | }
117 | };
118 |
119 | return () => {
120 | blockly.WorkspaceDragger.prototype.startDrag = originalStartDrag;
121 | blockly.WorkspaceDragger.prototype.drag = originalDrag;
122 | blockly.WorkspaceDragger.prototype.endDrag = originalEndDrag;
123 | };
124 | };
125 |
126 | export default enableTouchZoom;
127 |
--------------------------------------------------------------------------------
/src/plugins/mobile-code-batch-select/useKeyDownOperate.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { isCtrlKeyDown } from "utils/index";
3 | import { copyBatchedElements, pasteBatchedElements } from "utils/block-helper";
4 | import { SelectedElements } from "./useBatchSelect";
5 |
6 | const useKeyDownOperate: (params: { blockly: any; workspace: Blockly.WorkspaceSvg; vm: VirtualMachine }) => null = ({
7 | blockly,
8 | workspace,
9 | vm,
10 | }) => {
11 | const mousemoveRef = useRef<{ clientX: number; clientY: number }>({
12 | clientX: 0,
13 | clientY: 0,
14 | });
15 |
16 | const mousemove = (e) => {
17 | mousemoveRef.current = { clientX: e.clientX, clientY: e.clientY };
18 | };
19 |
20 | const onKeyDown = (e) => {
21 | if (blockly.locked) {
22 | return;
23 | }
24 | let batchDeleteBlocks = false;
25 | let batchCopyBlocks = false;
26 |
27 | const batchSelectedElements: SelectedElements = blockly.batchSelectedElements || [{}, {}];
28 | const selectedBlocks = Object.values(batchSelectedElements[0]);
29 | const selectedFrames = Object.values(batchSelectedElements[1]);
30 | const selectedElements = [...selectedBlocks, ...selectedFrames];
31 | if (e.keyCode === 8 || e.keyCode === 46) {
32 | if (selectedElements.length === 0) {
33 | // 没有选中的元素则return
34 | return;
35 | }
36 | // delete and backspace
37 | batchDeleteBlocks = true;
38 | }
39 | if (isCtrlKeyDown(e)) {
40 | if (e.keyCode == 86 && blockly.clipboardBatchElements?.length > 0) {
41 | // 'ctrl + v'
42 | pasteBatchedElements(mousemoveRef.current, workspace, blockly.clipboardBatchElements, vm);
43 | } else if (selectedElements.length === 0) {
44 | // 没有批量选中的block,并且按键按到了ctrl c 或者ctrl v 清除已记录的值。
45 | if (e.keyCode === 67 || e.keyCode === 88) {
46 | blockly.clipboardBatchElements = [];
47 | }
48 | return;
49 | } else if (e.keyCode === 67) {
50 | // 'ctrl + c'
51 | batchCopyBlocks = true;
52 | } else if (e.keyCode === 88) {
53 | // 'ctrl + x'
54 | batchCopyBlocks = true;
55 | batchDeleteBlocks = true;
56 | }
57 | }
58 |
59 | if (batchCopyBlocks) {
60 | blockly.clipboardBatchElements = copyBatchedElements(blockly.batchSelectedElements as SelectedElements);
61 | }
62 |
63 | if (batchDeleteBlocks) {
64 | blockly.Events.setGroup(true);
65 | selectedBlocks.forEach((bl) => {
66 | setTimeout(function () {
67 | blockly.mainWorkspace.fireDeletionListeners(bl);
68 | });
69 | bl.dispose(true, true);
70 | });
71 | selectedFrames.forEach((frame) => {
72 | frame.dispose();
73 | });
74 | blockly.batchSelectedElements = null;
75 | blockly.Events.setGroup(false);
76 | }
77 | };
78 |
79 | useEffect(() => {
80 | document.addEventListener("keydown", onKeyDown);
81 | document.addEventListener("mousemove", mousemove);
82 | return () => {
83 | document.removeEventListener("keydown", onKeyDown);
84 | document.addEventListener("mousemove", mousemove);
85 | };
86 | }, []);
87 |
88 | return null;
89 | };
90 |
91 | export default useKeyDownOperate;
92 |
--------------------------------------------------------------------------------
/src/plugins/mobile-code-batch-select/useRightContextMenu.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import type { IntlShape } from "react-intl";
3 | import { copyBatchedElements, pasteBatchedElements } from "utils/block-helper";
4 | import { SelectedElements } from "./useBatchSelect";
5 |
6 | interface IProps {
7 | workspace: Blockly.WorkspaceSvg;
8 | blockly: any;
9 | clearAllBoxedElements?: (boolean) => void;
10 | intl: IntlShape;
11 | vm: VirtualMachine;
12 | }
13 |
14 | const useBatchSelectRightMenu = ({ workspace, blockly, clearAllBoxedElements, intl, vm }: IProps) => {
15 | useEffect(() => {
16 | const menuItemId = window.Blockly.ContextMenu.addDynamicMenuItem(
17 | (items, element) => {
18 | if (element.boxed) {
19 | const menus = [
20 | {
21 | id: "Copy all",
22 | text: intl.formatMessage({
23 | id: "plugins.codeBatchSelect.duplicate.all",
24 | }),
25 | enabled: true,
26 | callback: () => {
27 | if (!blockly.batchSelectedElements) return;
28 | blockly.clipboardBatchElements = copyBatchedElements(blockly.batchSelectedElements as SelectedElements);
29 | },
30 | },
31 | {
32 | id: "Delete all",
33 | text: intl.formatMessage({
34 | id: "plugins.codeBatchSelect.delete.all",
35 | }),
36 | enabled: true,
37 | callback: () => {
38 | if (!blockly.batchSelectedElements) return;
39 | blockly.Events.setGroup(true);
40 | Object.values(blockly.batchSelectedElements[0]).forEach((bl: Blockly.Block) => {
41 | setTimeout(function () {
42 | blockly.mainWorkspace.fireDeletionListeners(bl);
43 | });
44 | bl.dispose(true, true);
45 | });
46 | Object.values(blockly.batchSelectedElements[1]).forEach((frame: Blockly.Frame) => {
47 | frame.dispose();
48 | });
49 | blockly.Events.setGroup(false);
50 | clearAllBoxedElements(true);
51 | },
52 | },
53 | ];
54 | return items.splice(0, items.length, ...menus);
55 | }
56 | return items;
57 | },
58 | {
59 | targetNames: ["frame", "blocks"],
60 | },
61 | );
62 | return () => {
63 | window.Blockly.ContextMenu.deleteDynamicMenuItem(menuItemId);
64 | };
65 | }, []);
66 |
67 | useEffect(() => {
68 | const menuItemId = window.Blockly.ContextMenu.addDynamicMenuItem(
69 | (items, _, event) => {
70 | if (blockly.clipboardBatchElements?.length > 0) {
71 | items.splice(2, 0, {
72 | id: "Paste all",
73 | text: intl.formatMessage({
74 | id: "plugins.codeBatchSelect.paste.all",
75 | }),
76 | enabled: true,
77 | callback: () => {
78 | pasteBatchedElements(event, workspace, blockly.clipboardBatchElements, vm);
79 | },
80 | });
81 | }
82 | return items;
83 | },
84 | {
85 | targetNames: ["workspace"],
86 | },
87 | );
88 | return () => {
89 | window.Blockly.ContextMenu.deleteDynamicMenuItem(menuItemId);
90 | };
91 | }, []);
92 | return null;
93 | };
94 |
95 | export default useBatchSelectRightMenu;
96 |
--------------------------------------------------------------------------------
/src/plugins/plugins-manager/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "plugins-manager",
3 | type: "component",
4 | description: "Control the loading and unloading of all plugins.",
5 | credits: [
6 | {
7 | name: "Luka",
8 | },
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/src/plugins/plugins-manager/styles.less:
--------------------------------------------------------------------------------
1 | .more {
2 | padding: 0 4px;
3 | color: var(--theme-brand-color);
4 | }
5 |
6 | .more:hover {
7 | text-decoration: underline;
8 | }
--------------------------------------------------------------------------------
/src/plugins/statistics/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import StatisticsIcon from "assets/icon--statistics.svg";
3 |
4 | const Statistics: React.FC = ({ redux, msg, registerSettings }) => {
5 | React.useEffect(() => {
6 | redux.dispatch({
7 | type: "scratch-gui/settings/SWITCH_STATISTICS_STATUS",
8 | open: true,
9 | });
10 | const register = registerSettings(
11 | msg("plugins.statistics.title"),
12 | "plugin-statistics",
13 | [
14 | {
15 | key: "enabledFunctions",
16 | label: msg("plugins.statistics.title"),
17 | description: msg("plugins.statistics.description"),
18 | items: [
19 | {
20 | key: "dropdown",
21 | label: msg("plugins.statistics.option.countCodeNum"),
22 | type: "switch",
23 | value: false,
24 | onChange: (value: boolean) => {
25 | redux.dispatch({
26 | type: "scratch-gui/settings/SWITCH_STATISTICS_CODE_COUNT",
27 | open: value,
28 | });
29 | },
30 | },
31 | {
32 | key: "input",
33 | label: msg("plugins.statistics.option.projectSize"),
34 | type: "switch",
35 | value: false,
36 | onChange: (value: boolean) => {
37 | redux.dispatch({
38 | type: "scratch-gui/settings/SWITCH_STATISTICS_BYTE_SIZE",
39 | open: value,
40 | });
41 | },
42 | },
43 | ],
44 | },
45 | ],
46 | ,
47 | );
48 | return () => {
49 | redux.dispatch({
50 | type: "scratch-gui/settings/SWITCH_STATISTICS_STATUS",
51 | open: false,
52 | });
53 | register.dispose();
54 | };
55 | }, [redux, registerSettings, msg]);
56 |
57 | return null;
58 | };
59 |
60 | Statistics.displayName = "Statistics";
61 |
62 | export default Statistics;
63 |
--------------------------------------------------------------------------------
/src/plugins/statistics/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "statistics",
3 | type: "component",
4 | description: "Count the number of blocks and file size of the project.",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/terminal/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import { defineMessages } from "@formatjs/intl";
4 | import Tooltip from "components/Tooltip";
5 | import { hotkeyIsDown, transitionHotkeysToString } from "utils/hotkey-helper";
6 |
7 | import TerminalIcon from "assets/icon--terminal.svg";
8 | import styles from "./styles.less";
9 |
10 | const messages = defineMessages({
11 | title: {
12 | id: "plugins.terminal.title",
13 | defaultMessage: "Terminal",
14 | description: "Terminal title",
15 | },
16 | });
17 |
18 | const DEFAULT_SETTINGS = {
19 | hotkeys: {
20 | visible: ["altKey", "T"],
21 | },
22 | };
23 |
24 | const Terminal: React.FC = ({ intl, vm, registerSettings, trackEvents }) => {
25 | const [shortcutKey, setShortcutKey] = React.useState(DEFAULT_SETTINGS.hotkeys.visible);
26 |
27 | const rootRef = React.useRef(null);
28 |
29 | const handleShow = React.useCallback(() => {
30 | trackEvents.dispatch(trackEvents.USE_ADDON, {
31 | searchType: "terminal",
32 | });
33 | vm.runtime.logSystem.show();
34 | }, [vm]);
35 |
36 | React.useEffect(() => {
37 | if (shortcutKey.length) {
38 | const handler = (e: KeyboardEvent) => {
39 | if (!rootRef.current.getBoundingClientRect().x) return;
40 | if (hotkeyIsDown(shortcutKey, e)) {
41 | e.preventDefault();
42 | handleShow();
43 | }
44 | };
45 | window.addEventListener("keydown", handler);
46 | return () => {
47 | window.removeEventListener("keydown", handler);
48 | };
49 | }
50 | }, [shortcutKey]);
51 |
52 | React.useEffect(() => {
53 | const register = registerSettings(intl.formatMessage(messages.title), "plugin-terminal", [
54 | {
55 | key: "hotkeys",
56 | label: "快捷键",
57 | items: [
58 | {
59 | key: "visible",
60 | type: "hotkey",
61 | label: intl.formatMessage(messages.title),
62 | value: shortcutKey,
63 | onChange: (value: Array) => {
64 | setShortcutKey(value);
65 | },
66 | },
67 | ],
68 | },
69 | ]);
70 | return () => {
71 | register.dispose();
72 | };
73 | }, [registerSettings]);
74 |
75 | return ReactDOM.createPortal(
76 |
77 | }
80 | onClick={handleShow}
81 | tipText={intl.formatMessage(messages.title)}
82 | shortcutKey={transitionHotkeysToString(shortcutKey)}
83 | />
84 | ,
85 | document.querySelector(".plugins-wrapper"),
86 | );
87 | };
88 |
89 | Terminal.displayName = "TerminalPlugin";
90 |
91 | export default Terminal;
92 |
--------------------------------------------------------------------------------
/src/plugins/terminal/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Terminal",
3 | type: "component",
4 | description: "The console lets you log running variables to make debugging easier.",
5 | credits: [
6 | {
7 | name: "Luka@CCW",
8 | link: "https://www.ccw.site/student/60d986a9fa5edd0db16a111f",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/terminal/styles.less:
--------------------------------------------------------------------------------
1 | .terminal-root {
2 | position: relative;
3 | }
4 |
5 | .search-icon {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | width: 32px;
10 | height: 32px;
11 | border-radius: 4px;
12 | cursor: pointer;
13 | }
14 |
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/components/MemberList/MemberListItem.less:
--------------------------------------------------------------------------------
1 | .member-list-item {
2 | display: flex;
3 | align-items: center;
4 | height: 48px;
5 | cursor: pointer;
6 | transition: background-color 0.3s;
7 | .member-list-item-action svg {
8 | color: var(--voice-plugin-button) !important;
9 | }
10 | }
11 |
12 | .member-list-item:hover {
13 | background-color: rgba(45, 140, 255, 1);
14 | .member-list-item-action svg {
15 | color: #fff !important;
16 | }
17 | .member-list-item-name {
18 | color: #fff !important;
19 | }
20 | }
21 |
22 |
23 | .member-list-item-info {
24 | flex: 1;
25 | display: flex;
26 | align-items: center;
27 | padding-left: 16px;
28 | gap: 13px;
29 | }
30 |
31 | .member-list-item-name {
32 | font-size: 12px;
33 | color: var(--theme-text-primary);
34 | white-space: nowrap;
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | line-height: 18px; /* 150% */
38 | font-weight: 400;
39 | width: 170px;
40 | }
41 |
42 | .member-list-item-action {
43 | display: flex;
44 | align-items: center;
45 | justify-content: center;
46 | cursor: pointer;
47 | height: 24px;
48 | }
49 |
50 | .local {
51 | color: var(---text-gray-500, #6B7280) !important;
52 | font-feature-settings: 'rclt' off;
53 | font-family: "PingFang SC";
54 | font-size: 12px;
55 | font-style: normal;
56 | font-weight: 400;
57 | line-height: 18px;
58 | }
59 |
60 | .override-menu {
61 | .chakra-menu__menu-list {
62 | .button {
63 | div:hover {
64 | background: var(--theme-brand-color) !important;
65 | span {
66 | color: #fff !important;
67 | }
68 | }
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/components/VoiceFloating/VoiceFloating.less:
--------------------------------------------------------------------------------
1 | .floating-window {
2 | z-index: 9999;
3 | position: absolute;
4 | }
5 |
6 | .text-container {
7 | white-space: nowrap;
8 | overflow: hidden;
9 | text-overflow: ellipsis;
10 | color: var(--theme-text-primary);
11 | padding: 0px 5px
12 | }
13 |
14 | .button {
15 | background: rgba(0,0,0,0) !important;
16 | }
17 |
18 | .button:hover {
19 | background: var(--theme-color-50) !important;
20 | }
21 |
22 | .leave-button {
23 | background-color: rgba(0,0,0,0) !important;
24 | }
25 |
26 | .leave-button:hover {
27 | background-color: var(--gandi-colors-red-600) !important;
28 | }
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/components/VoiceFloatingNew/VoiceFloatingNew.less:
--------------------------------------------------------------------------------
1 | .title {
2 | color: var(--theme-text-primary);
3 | font-family: "PingFang SC";
4 | font-size: 14px;
5 | font-style: normal;
6 | font-weight: 500;
7 | line-height: 22px;
8 | /* 157.143% */
9 | }
10 |
11 | .control-button {
12 | display: flex;
13 | width: 24px !important;
14 | min-width: 24px !important;
15 | height: 24px !important;
16 | padding: 7px !important;
17 | justify-content: center;
18 | align-items: center;
19 | flex-shrink: 0;
20 | background: rgba(0, 0, 0, 0) !important;
21 | background-color: rgba(0, 0, 0, 0) !important;
22 | border: none !important;
23 | }
24 |
25 | .control-button:hover {
26 | border-radius: 4px !important;
27 | background: var(---Dark-50, rgba(156, 163, 175, 0.15)) !important;
28 | }
29 |
30 | .titleBox {
31 | display: flex;
32 | justify-content: space-between;
33 | align-items: center;
34 | padding: 8px 16px;
35 | cursor: move;
36 | }
37 |
38 | .divider {
39 | width: 100%;
40 | height: 1px;
41 | background: var(--voice-plugin-divider);
42 | }
43 |
44 | .leave-button {
45 | color: var(---text-gray-400, #9CA3AF) !important;
46 | font-family: "PingFang SC";
47 | font-size: 14px;
48 | font-style: normal;
49 | font-weight: 400 !important;
50 | line-height: normal;
51 | border: none !important;
52 | // 居右
53 | float: right;
54 | }
55 |
56 | .leave-button:hover {
57 | color: var(--red--, #FA594C) !important;
58 | background-color: transparent !important;
59 | transition: all 0.2s !important;
60 | }
61 |
62 | .container {
63 | position: fixed;
64 | top: 0;
65 | font-size: 12px;
66 | background: var(--theme-color-300);
67 | border: 1px solid var(--theme-color-200);
68 | border-radius: 8px;
69 | color: var(--theme-text-primary);
70 | display: flex;
71 | flex-direction: column;
72 | box-shadow: 0px 26px 52px 0px rgba(0, 0, 0, 0.15), 0px 6px 30px 0px rgba(0, 0, 0, 0.50);
73 | z-index: 101;
74 | width: 279px;
75 | }
76 |
77 | .member-container {
78 | margin-top: 8px;
79 | margin-bottom: 8px;
80 | }
81 |
82 | .minimal-container {
83 | display: inline-flex;
84 | justify-content: center;
85 | align-items: center;
86 | gap: 8px;
87 | }
88 |
89 | .avatar-group {
90 | height: 24px !important;
91 | }
92 |
93 | .username {
94 | color: var(--theme-text-primary, #FFF);
95 | font-feature-settings: 'rclt' off;
96 | font-family: "PingFang SC";
97 | font-size: 12px;
98 | font-style: normal;
99 | font-weight: 400;
100 | line-height: 18px; /* 150% */
101 | max-width: 89px;
102 | overflow: hidden;
103 | text-overflow: ellipsis;
104 | white-space: nowrap;
105 | }
106 |
107 | .tip {
108 | color: var(---text-gray-400, #9CA3AF);
109 | font-feature-settings: 'rclt' off;
110 | font-family: "PingFang SC";
111 | font-size: 12px;
112 | font-style: normal;
113 | font-weight: 400;
114 | line-height: 18px; /* 150% */
115 | }
116 |
117 | .speaking-tips-container {
118 | display: flex;
119 | align-items: center;
120 | gap: var(--Base-4, 4px);
121 | }
122 |
123 | .button-group {
124 | display: inline-flex;
125 | align-items: center;
126 | gap: 8px;
127 | }
128 |
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SERVER_URL: "wss://voice.ccw.site",
3 | };
4 |
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/dots.less:
--------------------------------------------------------------------------------
1 | .loading-dots {
2 | display: flex;
3 | justify-content: space-between;
4 | width: 18px;
5 | height: 18px;
6 | align-items: center;
7 | }
8 |
9 | .loading-dots div {
10 | width: 3px;
11 | height: 3px;
12 | border-radius: 50%;
13 | background-color: var(--theme-text-primary);
14 | opacity: 0;
15 | animation: loading 1.4s infinite;
16 | }
17 |
18 | .loading-dots div:nth-child(1) {
19 | animation-delay: 0s;
20 | }
21 |
22 | .loading-dots div:nth-child(2) {
23 | animation-delay: 0.2s;
24 | }
25 |
26 | .loading-dots div:nth-child(3) {
27 | animation-delay: 0.4s;
28 | }
29 |
30 | @keyframes loading {
31 |
32 | 0%,
33 | 80%,
34 | 100% {
35 | opacity: 0;
36 | }
37 |
38 | 40% {
39 | opacity: 1;
40 | }
41 | }
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/lib/livekit.ts:
--------------------------------------------------------------------------------
1 | import * as LiveKit from "livekit-client";
2 | import type { ReconnectContext, ReconnectPolicy } from "livekit-client/dist/src/room/ReconnectPolicy";
3 | import config from "../config";
4 |
5 | const VoiceServer = config.SERVER_URL; // localhost debug
6 | let globalReason = 0;
7 | type Callback = {
8 | (status: true, room: LiveKit.Room): void;
9 | (status: false): void;
10 | };
11 | async function connectToRoom(token: string, callback: Callback) {
12 | const room = new LiveKit.Room({
13 | dynacast: true, // optimize publish bandwidth and CPU for published tracks
14 | publishDefaults: {
15 | audioPreset: LiveKit.AudioPresets.telephone,
16 | stopMicTrackOnMute: true,
17 | },
18 | reconnectPolicy: new CustomReconnectPolicy(),
19 | });
20 | try {
21 | await room.connect(VoiceServer, token);
22 | await room.localParticipant.setMicrophoneEnabled(true);
23 | callback(true, room);
24 | } catch (error) {
25 | room.disconnect(true);
26 | console.error("failed to connect to room", error);
27 | callback(false);
28 | return null;
29 | }
30 |
31 | room.on(LiveKit.RoomEvent.Disconnected, (reason) => {
32 | if (reason === LiveKit.DisconnectReason.PARTICIPANT_REMOVED) {
33 | room.disconnect(true);
34 | globalReason = reason;
35 | }
36 | document.querySelectorAll(".voiceAudio").forEach((audio) => {
37 | audio.remove();
38 | });
39 | callback(false);
40 | });
41 | room.on(LiveKit.RoomEvent.Reconnecting, () => {
42 | globalReason == LiveKit.DisconnectReason.PARTICIPANT_REMOVED && room.disconnect(true);
43 | });
44 | return room;
45 | }
46 |
47 | const maxRetryDelay = 7000;
48 |
49 | const DEFAULT_RETRY_DELAYS_IN_MS = [
50 | 0,
51 | 300,
52 | 2 * 2 * 300,
53 | 3 * 3 * 300,
54 | 4 * 4 * 300,
55 | maxRetryDelay,
56 | maxRetryDelay,
57 | maxRetryDelay,
58 | maxRetryDelay,
59 | maxRetryDelay,
60 | ];
61 |
62 | class CustomReconnectPolicy implements ReconnectPolicy {
63 | private readonly _retryDelays: number[];
64 |
65 | constructor(retryDelays?: number[]) {
66 | this._retryDelays = retryDelays !== undefined ? [...retryDelays] : DEFAULT_RETRY_DELAYS_IN_MS;
67 | }
68 |
69 | public nextRetryDelayInMs(context: ReconnectContext): number | null {
70 | if (globalReason == LiveKit.DisconnectReason.PARTICIPANT_REMOVED) return null;
71 | if (context.retryCount >= this._retryDelays.length) return null;
72 |
73 | const retryDelay = this._retryDelays[context.retryCount];
74 | if (context.retryCount <= 1) return retryDelay;
75 |
76 | return retryDelay + Math.random() * 1_000;
77 | }
78 | }
79 |
80 | export { connectToRoom };
81 |
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "voice-cooperation",
3 | type: "component",
4 | description: "A plugin provided voice cooperation capabilities",
5 | credits: [
6 | {
7 | name: "Sparrow@CCW",
8 | link: "https://www.ccw.site/student/619b9141e0c34311283fd4d8",
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/src/plugins/voice-cooperation/styles.less:
--------------------------------------------------------------------------------
1 | // .dot(18px, 18px, 9px, var(--Text-Text500, #FFF), var(--Text-Text500, #FFF));
2 |
3 | .voice-button {
4 | // max-width: 130px !important;
5 | // transition: all 0.2s !important;
6 | // height: 36px !important;
7 | // flex-shrink: 0;
8 | // border-radius: 56px !important;
9 | // background: var(--green--, #39C66C) !important;
10 | // padding: 8px 6px;
11 | // margin-left: 8px;
12 | display: flex;
13 | padding: 8px !important;
14 | align-items: center;
15 | gap: 4px;
16 | border-radius: 6px;
17 | width: 32px !important;
18 | height: 32px !important;
19 | min-width: 32px !important;
20 | // background: var(--theme-color-200) !important;
21 | margin-left: 8px;
22 | border: none !important;
23 | box-sizing: border-box !important;
24 | transition: all 0.2s !important;
25 | }
26 |
27 | .voice-button:hover {
28 | transition: all 0.2s !important;
29 | border: none !important;
30 | background: var(--theme-color-200) !important;
31 | }
32 |
33 | .overlay {
34 | position: fixed;
35 | top: 0;
36 | left: 0;
37 | right: 0;
38 | bottom: 0;
39 | background: rgba(0, 0, 0, 0);
40 | display: flex;
41 | justify-content: center;
42 | align-items: center;
43 | width: 100%;
44 | height: 100%;
45 | z-index: 1000;
46 | }
47 |
48 | .button-icon {
49 | display: flex;
50 | align-items: center;
51 | width: 18px;
52 | height: 18px;
53 | }
54 |
55 | .voice-button-loading {
56 | background: var(--theme-color-200) !important;
57 | padding: 7px !important;
58 | }
59 |
60 | .voice-button-connected {
61 | border-radius: 6px;
62 | background: var(--red--, #FA594C) !important;
63 | transition: all 0.2s !important;
64 | display: flex;
65 | padding: 7px !important;
66 | align-items: center;
67 | gap: 4px;
68 | }
69 |
70 | .voice-button-connected:hover {
71 | background: linear-gradient(0deg, rgba(0, 0, 0, 0.20)0%, rgba(0, 0, 0, 0.20)100%), #FA594C !important;
72 | }
73 |
74 | .voice-root {
75 | position: relative;
76 | }
77 |
78 | .join {
79 | color: var(--theme-text-primary);
80 | text-align: center;
81 | font-family: "PingFang SC";
82 | font-size: 14px;
83 | font-style: normal;
84 | font-weight: 500;
85 | line-height: normal;
86 | }
87 |
88 | .voice-popover {
89 | background-color: var(--theme-color-300);
90 | }
--------------------------------------------------------------------------------
/src/plugins/witcat-blockinput/manifest.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "witcat-blockinput",
3 | type: "function",
4 | description: "Make your block`s input more colorful!",
5 | credits: [
6 | {
7 | name: "白猫@CCW",
8 | link: "https://www.ccw.site/student/6173f57f48cf8f4796fc860e",
9 | },
10 | {
11 | name: "nights@CCW",
12 | link: "https://www.ccw.site/student/612ed1533068ae6640de96f0",
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/witcat-blockinput/style.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gandi-IDE/gandi-plugins/ed67c6596967de3fa7e08bc3d96687f330b67c8b/src/plugins/witcat-blockinput/style.less
--------------------------------------------------------------------------------
/src/utils/block-flasher.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Helper class to flash a Blockly scratch block in the users workspace
3 | */
4 | class BlockFlasher {
5 | block: Blockly.Block | null;
6 | count: number;
7 | flashOn: boolean;
8 | timerId: NodeJS.Timeout;
9 | constructor() {
10 | this.block = null;
11 | this.timerId = null;
12 | this.flashOn = false;
13 | this.count = 0;
14 |
15 | this.start = this.start.bind(this);
16 | this.privateFlash = this.privateFlash.bind(this);
17 | }
18 |
19 | /**
20 | * FLash a block 3 times
21 | */
22 | start(block: Blockly.Block) {
23 | if (this.timerId) {
24 | clearTimeout(this.timerId);
25 | if (this.block?.svgPath_) {
26 | this.block.svgPath_.style.fill = "";
27 | }
28 | }
29 |
30 | this.count = 4;
31 | this.flashOn = true;
32 | this.block = block;
33 | this.privateFlash(block);
34 | }
35 |
36 | /**
37 | * Internal method to switch the colour of a block between light yellow and it's original colour
38 | */
39 | private privateFlash(block: Blockly.Block) {
40 | if (block.svgPath_) {
41 | block.svgPath_.style.fill = this.flashOn ? "#ffff80" : "";
42 | }
43 | this.flashOn = !this.flashOn;
44 | this.count--;
45 | if (this.count > 0) {
46 | this.timerId = setTimeout(() => this.privateFlash(this.block), 200);
47 | } else {
48 | this.timerId = null;
49 | this.block = null;
50 | }
51 | }
52 | }
53 |
54 | export default new BlockFlasher();
55 |
--------------------------------------------------------------------------------
/src/utils/dom-helper.ts:
--------------------------------------------------------------------------------
1 | export default class DomHelpers {
2 | events: Array;
3 |
4 | constructor() {
5 | this.events = [];
6 | }
7 |
8 | triggerDragAndDrop(
9 | selectorDrag: SVGPathElement,
10 | selectorDrop: HTMLElement,
11 | mouseXY: { x: number; y: number },
12 | shiftKey: boolean,
13 | ) {
14 | // function for triggering mouse events
15 | shiftKey = shiftKey || false;
16 |
17 | const fireMouseEvent = function (type: string, elem: EventTarget | null, centerX: number, centerY: number) {
18 | const evt = document.createEvent("MouseEvents");
19 | evt.initMouseEvent(type, true, true, window, 1, 1, 1, centerX, centerY, shiftKey, false, false, false, 0, elem);
20 | elem?.dispatchEvent(evt);
21 | };
22 |
23 | // fetch target elements
24 | const elemDrag = selectorDrag; // document.querySelector(selectorDrag);
25 | const elemDrop = selectorDrop; // document.querySelector(selectorDrop);
26 | if (!elemDrag /* || !elemDrop*/) {
27 | return false;
28 | }
29 |
30 | // calculate positions
31 | let pos = elemDrag.getBoundingClientRect();
32 | const center1X = Math.floor((pos.left + pos.right) / 2);
33 | const center1Y = Math.floor((pos.top + pos.bottom) / 2);
34 |
35 | // mouse over dragged element and mousedown
36 | fireMouseEvent("mouseover", elemDrag, center1X, center1Y);
37 | fireMouseEvent("mousedown", elemDrag, center1X, center1Y);
38 |
39 | // start dragging process over to drop target
40 | fireMouseEvent("dragstart", elemDrag, center1X, center1Y);
41 | fireMouseEvent("drag", elemDrag, center1X, center1Y);
42 | fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
43 |
44 | if (!elemDrop) {
45 | if (mouseXY) {
46 | const center2X = mouseXY.x;
47 | const center2Y = mouseXY.y;
48 | fireMouseEvent("drag", elemDrag, center2X, center2Y);
49 | fireMouseEvent("mousemove", elemDrag, center2X, center2Y);
50 | }
51 | return false;
52 | }
53 |
54 | pos = elemDrop.getBoundingClientRect();
55 | const center2X = Math.floor((pos.left + pos.right) / 2);
56 | const center2Y = Math.floor((pos.top + pos.bottom) / 2);
57 |
58 | fireMouseEvent("drag", elemDrag, center2X, center2Y);
59 | fireMouseEvent("mousemove", elemDrop, center2X, center2Y);
60 |
61 | // trigger dragging process on top of drop target
62 | fireMouseEvent("mouseenter", elemDrop, center2X, center2Y);
63 | fireMouseEvent("dragenter", elemDrop, center2X, center2Y);
64 | fireMouseEvent("mouseover", elemDrop, center2X, center2Y);
65 | fireMouseEvent("dragover", elemDrop, center2X, center2Y);
66 |
67 | // release dragged element on top of drop target
68 | fireMouseEvent("drop", elemDrop, center2X, center2Y);
69 | fireMouseEvent("dragend", elemDrag, center2X, center2Y);
70 | fireMouseEvent("mouseup", elemDrag, center2X, center2Y);
71 |
72 | return true;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/utils/hotkey-helper.ts:
--------------------------------------------------------------------------------
1 | import { isMac } from "lib/client-info";
2 |
3 | export const transitionHotkeysToString = (keys: string[]) =>
4 | keys.map((i) => {
5 | switch (i) {
6 | case "ctrlKey":
7 | return isMac ? "Command" : "Ctrl";
8 | case "altKey":
9 | return isMac ? "Option" : "Alt";
10 | case "shiftKey":
11 | return "Shift";
12 | default:
13 | return i;
14 | }
15 | });
16 |
17 | export const FUNCTION_KEYS = {
18 | ctrlKey: isMac ? "metaKey" : "ctrlKey",
19 | altKey: "altKey",
20 | shiftKey: "shiftKey",
21 | };
22 |
23 | export const hotkeyIsDown = (keys: string[], event: KeyboardEvent) => {
24 | return keys.every((key) => {
25 | if (FUNCTION_KEYS[key]) {
26 | return event[FUNCTION_KEYS[key]];
27 | }
28 | return event.code === `Key${key}`;
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export const isCtrlKeyDown = (event): boolean => {
2 | return /macintosh|mac os x/i.test(navigator.userAgent) ? event.metaKey : event.ctrlKey;
3 | };
4 |
5 | const controlBorder = {
6 | top: 56,
7 | bottom: 40,
8 | left: 16,
9 | };
10 |
11 | export const isOverlap = (
12 | rect1: {
13 | x: number;
14 | y: number;
15 | width: number;
16 | height: number;
17 | },
18 | rect2: {
19 | x: number;
20 | y: number;
21 | width: number;
22 | height: number;
23 | },
24 | isControlBlock: boolean,
25 | workspace: { scale?: number },
26 | ) => {
27 | // 计算两个矩形的四个边界
28 | const left1 = rect1.x;
29 | const right1 = rect1.x + rect1.width;
30 | const top1 = rect1.y;
31 | const bottom1 = rect1.y + rect1.height;
32 |
33 | const left2 = rect2.x;
34 | const right2 = rect2.x + rect2.width;
35 | const top2 = rect2.y;
36 | const bottom2 = rect2.y + rect2.height;
37 | // 判断是否有重叠
38 | if (left1 > right2 || left2 > right1 || top1 > bottom2 || top2 > bottom1) {
39 | return false;
40 | } else {
41 | // control 类型的block跳过设置
42 | if (isControlBlock) {
43 | const blockLeft = left1 + controlBorder.left * workspace.scale;
44 | const blockTop = top1 + controlBorder.top * workspace.scale;
45 | const blockBottom = bottom1 - controlBorder.bottom * workspace.scale;
46 | if (left2 > blockLeft && blockBottom > bottom2 && blockTop < top2) {
47 | return false;
48 | }
49 | return true;
50 | }
51 | return true;
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/utils/name-helper.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Convert a string from spinal case to camel case.
3 | * @param {string} str - The string to convert.
4 | * @returns {string} The string converted to camel case.
5 | */
6 | export function spinalToCamel(str: string): string {
7 | return str.replace(/-([a-z])/g, function (match, letter: string) {
8 | return letter.toUpperCase();
9 | });
10 | }
11 |
12 | /**
13 | * Convert a spinal-case string to PascalCase.
14 | * @param {string} spinalCaseString - The spinal-case string to convert.
15 | * @returns {string} The resulting PascalCase string.
16 | */
17 | export function spinalToPascal(spinalCaseString: string): string {
18 | // Split the string by hyphens to get an array of words
19 | const words = spinalCaseString.split("-");
20 |
21 | // Capitalize the first letter of each word and join them together
22 | const pascalCaseString = words
23 | .map((word) => {
24 | // Capitalize the first letter of each word
25 | return word.charAt(0).toUpperCase() + word.slice(1);
26 | })
27 | .join("");
28 |
29 | return pascalCaseString;
30 | }
31 |
32 | /**
33 | * Extracts the file name from a given string containing both file name and extension.
34 | * @param {string} nameExt - The string containing the file name and extension.
35 | * @returns {string} The extracted file name.
36 | */
37 | export const extractFileName = (nameExt: string): string => nameExt.split(".", 1)[0];
38 |
39 | /**
40 | * Converts a string from camelCase to kebab-case.
41 | * @param {string} input - The camelCase string to be converted.
42 | * @returns {string} The kebab-case version of the input string.
43 | */
44 | export const camelToKebab = (input: string): string => {
45 | return input.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
46 | };
47 |
--------------------------------------------------------------------------------
/src/utils/workspace-utils.ts:
--------------------------------------------------------------------------------
1 | export const getBlockly = (workspace: Blockly.Workspace) => {
2 | return workspace.getScratchBlocks();
3 | };
4 |
--------------------------------------------------------------------------------
/src/utils/xml.ts:
--------------------------------------------------------------------------------
1 | export default class XML {
2 | xmlDoc = document.implementation.createDocument(null, "xml");
3 |
4 | newXml(root: HTMLElement, tagName: string, attrs: Record) {
5 | const element = this.xmlDoc.createElement(tagName);
6 | root.appendChild(element);
7 | return this.setAttr(element, attrs);
8 | }
9 |
10 | setAttr(root: HTMLElement, attrs: Record) {
11 | if (attrs) {
12 | for (const key of Object.keys(attrs)) {
13 | if (key === "text") {
14 | root.appendChild(this.xmlDoc.createTextNode(attrs[key]));
15 | } else {
16 | root.setAttribute(key, attrs[key]);
17 | }
18 | }
19 | }
20 | return root;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src"],
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "allowSyntheticDefaultImports": true,
6 | "baseUrl": ".",
7 | "declaration": true,
8 | "declarationDir": "lib",
9 | "module": "ESNext",
10 | "moduleResolution": "node",
11 | "outDir": "./dist",
12 | "paths": {
13 | "plugins/*": ["src/plugins/*"],
14 | "assets/*": ["./src/assets/*"],
15 | "lib/*": ["./src/lib/*"],
16 | "utils/*": ["./src/utils/*"],
17 | "components/*": ["./src/components/*"],
18 | "hooks/*": ["./src/hooks/*"],
19 | "types/*": ["./src/types/*"]
20 | },
21 | "resolveJsonModule": true,
22 | "sourceMap": true,
23 | "target": "ES2017",
24 | "jsx": "react"
25 | },
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 | const CopyWebpackPlugin = require("copy-webpack-plugin");
4 |
5 | module.exports = {
6 | mode: "production",
7 | entry: {
8 | main: "./src/main.ts",
9 | playground: "./src/index.tsx",
10 | },
11 | output: {
12 | path: path.join(__dirname, "dist"),
13 | libraryTarget: "umd",
14 | library: "GandiPlugins",
15 | filename: "static/js/[name].js",
16 | chunkFilename: "[name].[hash:5].js",
17 | clean: true, // Clean the output directory before emit.
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.tsx?$/,
23 | use: "ts-loader",
24 | exclude: /node_modules/,
25 | },
26 | {
27 | test: /\.less$/,
28 | use: [
29 | "style-loader",
30 | {
31 | loader: "@teamsupercell/typings-for-css-modules-loader",
32 | },
33 | {
34 | loader: "css-loader",
35 | options: {
36 | sourceMap: true,
37 | modules: {
38 | localIdentName: "addons_[local]_[hash:base64:5]",
39 | exportLocalsConvention: "camelCaseOnly",
40 | },
41 | },
42 | },
43 | {
44 | loader: "postcss-loader",
45 | },
46 | {
47 | loader: "less-loader",
48 | },
49 | ],
50 | },
51 | {
52 | test: /\.css$/i,
53 | use: ["style-loader", "css-loader"],
54 | },
55 | {
56 | test: /\.svg$/i,
57 | type: "asset",
58 | resourceQuery: /url/, // *.svg?url
59 | },
60 | {
61 | test: /\.svg$/i,
62 | issuer: /\.[jt]sx?$/,
63 | resourceQuery: { not: [/url/] }, // exclude react component if *.svg?url
64 | use: ["@svgr/webpack"],
65 | },
66 | ],
67 | },
68 | resolve: {
69 | extensions: [".tsx", ".ts", ".js"],
70 | alias: {
71 | src: path.resolve(__dirname, "./src"),
72 | plugins: path.resolve(__dirname, "./src/plugins"),
73 | assets: path.resolve(__dirname, "./src/assets"),
74 | lib: path.resolve(__dirname, "./src/lib"),
75 | utils: path.resolve(__dirname, "./src/utils"),
76 | components: path.resolve(__dirname, "./src/components"),
77 | hooks: path.resolve(__dirname, "./src/hooks"),
78 | types: path.resolve(__dirname, "./src/types"),
79 | },
80 | },
81 | plugins: [
82 | new HtmlWebpackPlugin({
83 | chunks: ["playground"],
84 | template: "./index.html",
85 | title: "Gandi Plugins",
86 | }),
87 | new CopyWebpackPlugin({
88 | patterns: [
89 | {
90 | from: "./favicon.ico",
91 | to: "./",
92 | },
93 | ],
94 | }),
95 | ],
96 | optimization: {
97 | minimize: true,
98 | },
99 | target: ["web", "es6"],
100 | };
101 |
--------------------------------------------------------------------------------
/webpackDevServer.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 | const CopyWebpackPlugin = require("copy-webpack-plugin");
4 | const webpack = require("webpack");
5 |
6 | module.exports = {
7 | mode: "development",
8 | devtool: "cheap-module-source-map",
9 | entry: {
10 | "lib.min": ["react", "react-dom"],
11 | main: "./src/main.ts",
12 | playground: "./src/index.tsx",
13 | },
14 | output: {
15 | path: path.join(__dirname, "dist"),
16 | libraryTarget: "umd",
17 | library: "GandiPlugins",
18 | filename: "[name].js",
19 | clean: true, // Clean the output directory before emit.
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.tsx?$/,
25 | use: "ts-loader",
26 | exclude: /node_modules/,
27 | },
28 | {
29 | test: /\.less$/,
30 | use: [
31 | "style-loader",
32 | {
33 | loader: "@teamsupercell/typings-for-css-modules-loader",
34 | },
35 | {
36 | loader: "css-loader",
37 | options: {
38 | sourceMap: true,
39 | modules: {
40 | localIdentName: "addons_[local]_[hash:base64:5]",
41 | exportLocalsConvention: "camelCaseOnly",
42 | },
43 | },
44 | },
45 | {
46 | loader: "postcss-loader",
47 | },
48 | {
49 | loader: "less-loader",
50 | },
51 | ],
52 | },
53 | {
54 | test: /\.css$/i,
55 | use: ["style-loader", "css-loader"],
56 | },
57 | {
58 | test: /\.svg$/i,
59 | type: "asset",
60 | resourceQuery: /url/, // *.svg?url
61 | },
62 | {
63 | test: /\.svg$/i,
64 | issuer: /\.[jt]sx?$/,
65 | resourceQuery: { not: [/url/] }, // exclude react component if *.svg?url
66 | use: ["@svgr/webpack"],
67 | },
68 | ],
69 | },
70 | resolve: {
71 | extensions: [".tsx", ".ts", ".js"],
72 | alias: {
73 | src: path.resolve(__dirname, "./src"),
74 | plugins: path.resolve(__dirname, "./src/plugins"),
75 | assets: path.resolve(__dirname, "./src/assets"),
76 | lib: path.resolve(__dirname, "./src/lib"),
77 | utils: path.resolve(__dirname, "./src/utils"),
78 | components: path.resolve(__dirname, "./src/components"),
79 | hooks: path.resolve(__dirname, "./src/hooks"),
80 | types: path.resolve(__dirname, "./src/types"),
81 | },
82 | },
83 | watchOptions: {
84 | ignored: ["/node_modules", "/dist", "/lib"],
85 | },
86 | devServer: {
87 | headers: {
88 | "Access-Control-Allow-Origin": "*",
89 | },
90 | compress: true,
91 | port: 8081,
92 | },
93 | plugins: [
94 | new HtmlWebpackPlugin({
95 | chunks: ["lib.min", "main", "playground"],
96 | template: "./index.html",
97 | title: "Gandi Plugins",
98 | }),
99 | new CopyWebpackPlugin({
100 | patterns: [
101 | {
102 | from: "./favicon.ico",
103 | to: "./",
104 | },
105 | ],
106 | }),
107 | new webpack.DefinePlugin({
108 | "process.env.SITE_SRC":
109 | process.env.SITE === "COCREA" ? '"https://cocrea.world/gandi"' : '"https://ccw.site/gandi"',
110 | }),
111 | ],
112 | target: ["web", "es6"],
113 | };
114 |
--------------------------------------------------------------------------------