Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/format-code.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Format code using Prettier
on:
push:
# Please also update the paths listed on .prettierignore.
paths-ignore:
- libraries/thirdparty/*
- '*.md'
Expand Down Expand Up @@ -31,6 +30,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# 修改這裡:讓機器人明確知道要把改好的程式碼推回哪個分支
ref: ${{ github.head_ref || github.ref }}

- name: Generate token
uses: tibdex/github-app-token@v2
Expand All @@ -55,8 +57,10 @@ jobs:
if: steps.generate-token.outcome != 'success'
uses: ScratchAddons/prettier_action@master
with:
# 這裡我幫你把 push 選項補得更穩一點
prettier_options: --write .
prettier_version: 3.2.5
commit_message: Format code
only_changed: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13 changes: 13 additions & 0 deletions addons-l10n/en/editor-input-shadow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"editor-input-shadow/@name": "all Detache",
"editor-input-shadow/@description": "Adds a \"Toggle\" button to the context menu for separating and joining input slots.",
"editor-input-shadow/@info-Warning": "Using blocks not explicitly allowed by Scratch may risk project unavailability in future versions, project deletion, or account bans.\nScratch Addons is not responsible for this; please use this feature with caution.",
"editor-input-shadow/@settings-name-spawn_range_x": "Block Spawn X Range",
"editor-input-shadow/@settings-name-spawn_range_y": "Block Spawn Y Range",
"editor-input-shadow/@settings-name-full_shadow_lock": "Enable Recursive Shadow/Unshadow Functionality",
"editor-input-shadow/@credits-GarboMuffin": "Reference Their Block switching code",
"editor-input-shadow/@credits-pufferfish101007": "Reference Their Block switching code",
"editor-input-shadow/text-shadow-switch": "Toggle Input Slot Lock",
"editor-input-shadow/text-all-to-shadow": "Shadow All Connected Blocks",
"editor-input-shadow/text-all-to-nonshadow": "Unshadow All Connected Blocks"
}
13 changes: 13 additions & 0 deletions addons-l10n/zh-tw/editor-input-shadow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"editor-input-shadow/@name": "萬物拆卸",
"editor-input-shadow/@description": "新增\"切換\"按鈕到右鍵選單,用於分離與接合輸入格",
"editor-input-shadow/@info-Warning": "使用未被Scratch官方明確允許的積木可能面臨後續版本不可用、專案被移除甚至封號的風險。\nScratch Addons並不為此負責,請謹慎使用此功能。",
"editor-input-shadow/@settings-name-spawn_range_x": "積木噴灑 X 軸範圍",
"editor-input-shadow/@settings-name-spawn_range_y": "積木噴灑 Y 軸範圍",
"editor-input-shadow/@settings-name-full_shadow_lock": "啟用範圍影化/實化功能",
"editor-input-shadow/@credits-GarboMuffin": "參考了他們的 程式積木切換 程式",
"editor-input-shadow/@credits-pufferfish101007": "參考了他們的 程式積木切換 程式",
"editor-input-shadow/text-shadow-switch": "切換輸入格鎖定",
"editor-input-shadow/text-all-to-shadow": "將連接的所有積木影化",
"editor-input-shadow/text-all-to-nonshadow": "將連接的所有積木實化"
}
3 changes: 2 additions & 1 deletion addons/addons.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,6 @@
"fullscreen",
"columns",
"editor-compact",
"editor-theme3"
"editor-theme3",
"editor-input-shadow"
]
69 changes: 69 additions & 0 deletions addons/editor-input-shadow/addon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"name": "all Detache",
"description": "Adds a \"Toggle\" button to the context menu for separating and joining input slots.",
"tags": [
"danger",
"editor",
"codeEditor",
"beta"
],
"info": [
{
"type": "notice",
"text": "Using blocks not explicitly allowed by Scratch may risk project unavailability in future versions, project deletion, or account bans.\nScratch Addons is not responsible for this; please use this feature with caution.",
"id": "Warning"
}
],
"credits": [
{
"id": "entropy_lover",
"name": "entropy_lover_nagnto",
"link": "https://scratch.mit.edu/users/entropy_lover/"
},
{
"id": "GarboMuffin",
"name": "GarboMuffin",
"note": "Reference Their Block switching code"
},
{
"id": "pufferfish101007",
"name": "pufferfish101007",
"note": "Reference Their Block switching code"
}
],
"userscripts": [
{
"url": "userscript.js",
"matches": [
"projects"
]
}
],
"dynamicEnable": true,
"dynamicDisable": true,
"versionAdded": "1.44.4",
"settings": [
{
"id": "spawn_range_x",
"name": "Block Spawn X Range",
"type": "integer",
"default": 0,
"min": 0,
"max": 200
},
{
"id": "spawn_range_y",
"name": "Block Spawn Y Range",
"type": "integer",
"default": 0,
"min": 0,
"max": 200
},
{
"id": "full_shadow_lock",
"name": "Enable Recursive Shadow/Unshadow Functionality",
"type": "boolean",
"default": true
}
]
}
177 changes: 177 additions & 0 deletions addons/editor-input-shadow/userscript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
export default async function ({ addon, msg }) {
const addonname = "editor-input-shadow";
const ScratchBlocks = await addon.tab.traps.getBlockly();
const workspace = await addon.tab.traps.getWorkspace();
if (!ScratchBlocks || !workspace) return console.error(`[${addonname}] ScratchBlocks 未載入`);

const cleanXml = (node) => {
if (node.nodeType !== 1) return;
node.removeAttribute('id');
node.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
};

const spawnAt = (xml, targetPos) => {
const b = ScratchBlocks.Xml.domToBlock(xml, workspace);
const myC = b.outputConnection || b.previousConnection;
if (myC && targetPos !== undefined) {
const cfg = {
rangeX: addon.settings.get('spawn_range_x'),
rangeY: addon.settings.get('spawn_range_y'),
get offX() { return -(this.rangeX / 2); },
get offY() { return -(this.rangeY / 2); }
};
const currentXY = b.getRelativeToSurfaceXY();
const connOffset = { x: myC.x_ - currentXY.x, y: myC.y_ - currentXY.y };
const rx = cfg.rangeX > 0 ? Math.floor(Math.random() * (cfg.rangeX + 1)) + cfg.offX : 0;
const ry = cfg.rangeY > 0 ? Math.floor(Math.random() * (cfg.rangeY + 1)) + cfg.offY : 0;
b.moveBy(targetPos.x - connOffset.x - currentXY.x + rx, targetPos.y - connOffset.y - currentXY.y + ry);
}
};

const input_lock = (block, opcodeData) => {
if (opcodeData.isNoop) return;
try {
ScratchBlocks.Events.setGroup(true);
const mainPos = block.getRelativeToSurfaceXY();
const xml = ScratchBlocks.Xml.blockToDom(block);
const spawnList = [];
const connPosMap = new Map();

const record = (b) => {
if (!b) return;
b.inputList.forEach(input => {
const conn = input.connection;
if (conn) connPosMap.set(input.name + b.id, { x: conn.x_, y: conn.y_ });
if (conn && conn.targetBlock()) record(conn.targetBlock());
});
if (b.getNextBlock()) record(b.getNextBlock());
};
record(block);

const process = (vNode, isFirst, parentId) => {
const sChild = vNode.querySelector(':scope > shadow');
const bChild = vNode.querySelector(':scope > block');
const name = vNode.getAttribute('name');
if (bChild) {
const bId = bChild.getAttribute('id');
const targetConnPos = connPosMap.get(name + parentId);
if (sChild && targetConnPos) {
const sToB = xml.ownerDocument.createElement('block');
for (let a of sChild.attributes) sToB.setAttribute(a.name, a.value);
while (sChild.firstChild) sToB.appendChild(sChild.firstChild);
spawnList.push({ xml: sToB, pos: targetConnPos });
}
const nS = xml.ownerDocument.createElement('shadow');
for (let a of bChild.attributes) nS.setAttribute(a.name, a.value);
while (bChild.firstChild) nS.appendChild(bChild.firstChild);
Array.from(nS.querySelectorAll('value')).forEach(v => process(v, false, bId));
vNode.innerHTML = ''; vNode.appendChild(nS);
} else if (sChild && isFirst) {
const nB = xml.ownerDocument.createElement('block');
for (let a of sChild.attributes) nB.setAttribute(a.name, a.value);
while (sChild.firstChild) nB.appendChild(sChild.firstChild);
Array.from(nB.querySelectorAll('value')).forEach(v => process(v, false, sChild.getAttribute('id') || ''));
vNode.replaceChild(nB, sChild);
}
};
Array.from(xml.childNodes).filter(n => n.tagName && n.tagName.toLowerCase() === 'value').forEach(v => process(v, true, block.id));

const parent = block.getParent();
const pConn = parent ? parent.getConnections_().find(c => c.targetConnection && c.targetConnection.sourceBlock_ === block) : null;
const bConnType = pConn ? block.getConnections_().find(c => c.targetConnection && c.targetConnection.sourceBlock_ === parent).type : null;
block.dispose();
const newBlock = ScratchBlocks.Xml.domToBlock(xml, workspace);
newBlock.moveBy(mainPos.x, mainPos.y);
if (pConn) newBlock.getConnections_().find(c => c.type === bConnType).connect(pConn);
spawnList.forEach(item => spawnAt(item.xml, item.pos));
} catch (e) { console.error(`[${addonname}]` + e); } finally { ScratchBlocks.Events.setGroup(false); }
};

const process_all = (block, mode) => {
try {
ScratchBlocks.Events.setGroup(true);
const mainPos = block.getRelativeToSurfaceXY();
const xml = ScratchBlocks.Xml.blockToDom(block);
const spawnList = [];
const connPosMap = new Map();

const record = (b) => {
if (!b) return;
b.inputList.forEach(input => {
if (input.connection) connPosMap.set(input.name + b.id, { x: input.connection.x_, y: input.connection.y_ });
if (input.connection && input.connection.targetBlock()) record(input.connection.targetBlock());
});
if (b.nextConnection) {
connPosMap.set('NEXT' + b.id, { x: b.nextConnection.x_, y: b.nextConnection.y_ });
if (b.getNextBlock()) record(b.getNextBlock());
}
};
record(block);

const transform = (element, parentId) => {
Array.from(element.querySelectorAll(':scope > value, :scope > statement, :scope > next')).forEach(container => {
const tagName = container.tagName.toLowerCase();
const sChild = container.querySelector(':scope > shadow');
const bChild = container.querySelector(':scope > block');
const name = container.getAttribute('name') || (tagName === 'next' ? 'NEXT' : '');
const targetPos = connPosMap.get(name + parentId);
if (mode === 'LOCK' && bChild) {
if (sChild && targetPos) {
const sToB = xml.ownerDocument.createElement('block');
for (let a of sChild.attributes) sToB.setAttribute(a.name, a.value);
while (sChild.firstChild) sToB.appendChild(sChild.firstChild);
spawnList.push({ xml: sToB, pos: targetPos });
}
const nS = xml.ownerDocument.createElement('shadow');
for (let a of bChild.attributes) nS.setAttribute(a.name, a.value);
while (bChild.firstChild) nS.appendChild(bChild.firstChild);
transform(nS, bChild.getAttribute('id'));
container.innerHTML = ''; container.appendChild(nS);
} else if (mode === 'UNLOCK' && (bChild || sChild)) {
if (bChild && sChild && targetPos) {
const sToB = xml.ownerDocument.createElement('block');
for (let a of sChild.attributes) if (a.name.toLowerCase() !== 'id') sToB.setAttribute(a.name, a.value);
Array.from(sChild.childNodes).forEach(node => sToB.appendChild(node.cloneNode(true)));
cleanXml(sToB);
transform(sToB, sChild.getAttribute('id'));
spawnList.push({ xml: sToB, pos: targetPos });
}
const target = bChild || sChild;
const nB = xml.ownerDocument.createElement('block');
for (let a of target.attributes) nB.setAttribute(a.name, a.value);
while (target.firstChild) nB.appendChild(target.firstChild);
const nextId = target.getAttribute('id');
container.innerHTML = ''; container.appendChild(nB);
transform(nB, nextId);
}
});
};
transform(xml, block.id);

const parent = block.getParent();
const pConn = parent ? parent.getConnections_().find(c => c.targetConnection && c.targetConnection.sourceBlock_ === block) : null;
const bConnType = pConn ? block.getConnections_().find(c => c.targetConnection && c.targetConnection.sourceBlock_ === parent).type : null;
block.dispose();
const newBlock = ScratchBlocks.Xml.domToBlock(xml, workspace);
newBlock.moveBy(mainPos.x, mainPos.y);
if (pConn) newBlock.getConnections_().find(c => c.type === bConnType).connect(pConn);
spawnList.forEach(item => spawnAt(item.xml, item.pos));
} catch (e) { console.error(`[${addonname}]` + e); } finally { ScratchBlocks.Events.setGroup(false); }
};

addon.tab.createBlockContextMenu((items, block) => {
if (!addon.self.disabled) {
items.push({
enabled: true,
text: msg("text-shadow-switch"),
callback: () => input_lock(block, { opcode: block.type, shadow: block.isShadow(), isNoop: false }),
separator: true,
});
if (addon.settings.get('full_shadow_lock')) {
items.push({ enabled: true, text: msg("text-all-to-shadow"), callback: () => process_all(block, 'LOCK') });
items.push({ enabled: true, text: msg("text-all-to-nonshadow"), callback: () => process_all(block, 'UNLOCK') });
}
return items;
}
}, { blocks: true });
}
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default [
"no-unused-vars": 0,
"no-useless-escape": 0,
"no-inner-declarations": 0,
"no-useless-assignment": 0,

"no-constant-condition": [
2,
Expand Down