// ==UserScript==
// @name App Inventer 2 block helper
// @version 0.7.8
// @namespace App Inventer 2 block helper
// @description An easy way to operate blocks at MIT App Inventor 2.
// @author wangsk789@163.com
// @match https://*.appinventor.mit.edu/*
// @match http://localhost/*
// @match http://192.168.*.*/*
// @license MIT
// ==/UserScript==
(function() {
//'use strict';
setTimeout(() => {
var lastBlock;
var types=["component_event","global_declaration","procedures_defreturn","procedures_defnoreturn"];
function genHelperPalette(){
var box = document.createElement('div');
document.querySelector(".ode-WorkColumns").appendChild(box);
box.outerHTML='<div class="ode-Box" aria-hidden="true" style="width: 220px; display: none;" id="helperPalette"><div class="ode-Box-content" ><table cellspacing="0" cellpadding="0" class="ode-Box-header" style="width: 100%;"><tbody><tr><td align="left" width="" height="" rowspan="1" style="vertical-align: top;"><div style="width: 100%;"><div class="ode-Box-header-caption" style="white-space: nowrap;">AI2HELPER </div></div></td></tr></tbody></table><div style="margin:3px"><input type="text" id="keyword" ></div><div tabindex="0" class="ode-TextButton" id="searchkeyword" style="margin:3px">search keyword</div><div tabindex="0" class="ode-TextButton" id="removeallcomments" style="margin:3px">remove all comments</div><div tabindex="0" class="ode-TextButton" id="downloadPNGIgnoreOrphan" style="margin:3px">download all as png</div><div tabindex="0" class="ode-TextButton" id="updateoutline" style="margin:3px">update outline</div><div id="helperOutline"></div></div></div>';
}
var isHelperOpen = false;
function switchHelper(){
var helper = document.querySelector("#helperPalette");
if(helper.style.display == "none"){
helper.style.display = "block"
updateoutline();
isHelperOpen = true;
}else{
helper.style.display = "none"
isHelperOpen = false;
}
}
function updateoutline(){
let blocks = Blockly.getMainWorkspace().getTopBlocks().filter((block) => types.indexOf(block.type)>-1);
blocks.sort((a,b) => a.toString().localeCompare(b.toString()));
var helperOutline = document.querySelector("#helperOutline");
helperOutline.innerHTML="";
blocks.forEach((block) =>{
let div = document.createElement('DIV');
helperOutline.appendChild(div);
//div.outerHTML='<div class="gwt-TreeItem" style="white-space: nowrap; padding:3px; overflow:hidden;" id="id'+block.id+'" onclick="btnonclick(this);">'+ simpleString(block)+'</div>';
//div.className = "gwt-TreeItem";
div.style = "white-space: nowrap; padding:3px; overflow:hidden;";
div.id = "id"+block.id;
div.addEventListener("click",() => {
btnonclick(div);
})
div.innerHTML = simpleString(block);
})
}
function btnonclick(obj){
if(lastBlock) {lastBlock.setHighlighted(false);}
var block =Blockly.getMainWorkspace().getBlockById(obj.id.substr(2));
if(block == lastBlock){
lastBlock = null;
}else{
Blockly.getMainWorkspace().centerOnBlock(block.id);
block.select();
block.setHighlighted(true);
lastBlock = block;
}
}
function simpleString(block) {
var text = '';
for (var i = 0, input;
(input = block.inputList[i]); i++) {
if (input.name == Blockly.BlockSvg.COLLAPSED_INPUT_NAME) {
continue;
}
for (var j = 0, field;
(field = input.fieldRow[j]); j++) {
text += field.getText() + ' ';
}
}
text = goog.string.trim(text) || '???';
return text;
}
function removeallcomments(){
if(confirm("Are you sure to remove all comments?")){
Blockly.getMainWorkspace().getAllBlocks().forEach(b=>{b.setCommentText(null)});
}
}
function searchkeyword(){
var input = document.querySelector("#keyword");
if(input.value){
findBlock(input.value);
}
}
var lastIndex = -1;
function findBlock(keyword){
var blocks = Blockly.getMainWorkspace().getAllBlocks().filter(block=>simpleString(block).toLowerCase().includes(keyword.toLowerCase()));
if(lastIndex > -1){
blocks[lastIndex].setHighlighted(false);
}
lastIndex++;
if(lastIndex<blocks.length){
expand(blocks[lastIndex]);
Blockly.getMainWorkspace().cleanUp()
Blockly.getMainWorkspace().centerOnBlock(blocks[lastIndex].id);
blocks[lastIndex].select();
blocks[lastIndex].setHighlighted(true);
}else{
lastIndex = -1;
}
}
function expand(block){
block.setCollapsed(false);
let parent = block.getParent();
if(parent){
expand(parent);
}
}
function downloadPNGIgnoreOrphan(){
var topblocks=Blockly.getMainWorkspace().getTopBlocks();
var blocks=topblocks.filter((block)=>{
return types.indexOf(block.type)>=0
});
if(confirm("Are you sure to download all " + blocks.length + " blocks?")){
var i=0;
var timer=setTimeout(function(){
if(i<blocks.length){
exportBlockAsPng(blocks[i]);
i++;
timer=setTimeout(arguments.callee,1000)
}
},1000);
}
}
function genHelperButton(){
let btnHelper = document.createElement('div');
let allRight = document.querySelectorAll(".right");
var container = allRight[allRight.length-1];
container.appendChild(btnHelper);
btnHelper.outerHTML='<div tabindex="0" class="ode-TextButton" id="helperButton">AI2HELPER</div>';
}
genHelperButton();
genHelperPalette();
document.querySelector("#removeallcomments").addEventListener("click",() => {
removeallcomments();
})
document.querySelector("#helperButton").addEventListener("click",() => {
switchHelper();
})
document.querySelector("#searchkeyword").addEventListener("click",() => {
searchkeyword();
})
document.querySelector("#downloadPNGIgnoreOrphan").addEventListener("click",() => {
downloadPNGIgnoreOrphan();
})
document.querySelector("#updateoutline").addEventListener("click",() => {
updateoutline();
})
//set designer panel scroll seperately
//document.querySelector(".ode-ProjectListView").childNodes[1].style.overflow = "auto";
//document.querySelector(".ode-ProjectListView").childNodes[1].style.height="100%";
//document.querySelector(".ode-TutorialWrapper").style.overflowY="hidden";
//document.querySelector(".ode-WorkColumns").childNodes.forEach(node => {node.style.height = "calc(100% - 38px)"; node.style.overflow = "auto"});
///////////////////////////////////////////////////////////////
///////below codes from MIT App Inventer source code///////////
///////////////////////////////////////////////////////////////
var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
function isExternal(url) {
return url && url.lastIndexOf('http', 0) == 0 && url.lastIndexOf(window.location.host) == -1
}
function styles(el, selectorRemap) {
var css = "";
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
if (isExternal(sheets[i].href)) {
console.warn("Cannot include styles from other hosts: " + sheets[i].href);
continue
}
var rules = null;
try {
rules = sheets[i].cssRules
} catch (e) {
console.warn('Skipping a potentially injected stylesheet', e);
continue
}
if (rules != null) {
for (var j = 0; j < rules.length; j++) {
var rule = rules[j];
if (typeof(rule.style) != "undefined") {
var match = null;
try {
match = el.querySelector(rule.selectorText)
} catch (err) {
console.warn('Invalid CSS selector "' + rule.selectorText + '"', err)
}
if (match && rule.selectorText.indexOf("blocklySelected") == -1) {
var selector = selectorRemap ? selectorRemap(rule.selectorText) : rule.selectorText;
css += selector + " { " + rule.style.cssText + " }"
} else if (rule.cssText.match(/^@font-face/)) {
css += rule.cssText + ''
}
}
}
}
}
return css
}
function svgAsDataUri(el, optmetrics, options, cb) {
options = options || {};
options.scale = options.scale || 1;
var xmlns = "http://www.w3.org/2000/xmlns/";
var outer = document.createElement("div");
var textAreas = document.getElementsByTagName("textarea");
for (var i = 0; i < textAreas.length; i++) {
textAreas[i].innerHTML = textAreas[i].value
}
var clone = el.cloneNode(true);
var width, height;
if (el.tagName == 'svg') {
var box = el.getBoundingClientRect();
width = box.width || parseInt(clone.getAttribute('width') || clone.style.width || window.getComputedStyle(el).getPropertyValue('width'));
height = box.height || parseInt(clone.getAttribute('height') || clone.style.height || window.getComputedStyle(el).getPropertyValue('height'));
var left = (parseFloat(optmetrics.contentLeft) - parseFloat(optmetrics.viewLeft)).toString();
var top = (parseFloat(optmetrics.contentTop) - parseFloat(optmetrics.viewTop)).toString();
var right = (parseFloat(optmetrics.contentWidth)).toString();
var bottom = (parseFloat(optmetrics.contentHeight)).toString();
clone.setAttribute("viewBox", left + " " + top + " " + right + " " + bottom)
} else {
var matrix = el.getScreenCTM();
//clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '').replace(/scale\(.*?\)/, '').trim());
clone.setAttribute('transform', "");
var box = el.getBBox();
width = box.width;
height = box.height;
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.appendChild(clone);
clone = svg;
clone.setAttribute('viewBox', box.x + " " + box.y + " " + width + " " + height)
}
clone.setAttribute("version", "1.1");
clone.setAttribute("width", width);
clone.setAttribute("height", height);
clone.setAttribute("style", 'background-color: rgba(255, 255, 255, 0);');
outer.appendChild(clone);
var css = styles(el, options.selectorRemap);
var s = document.createElement('style');
s.setAttribute('type', 'text/css');
s.innerHTML = "<![CDATA[" + css + "]]>";
var defs = document.createElement('defs');
defs.appendChild(s);
clone.insertBefore(defs, clone.firstChild);
var toHide = clone.getElementsByClassName("blocklyScrollbarHandle");
for (var i = 0; i < toHide.length; i++) {
toHide[i].setAttribute("visibility", "hidden")
}
toHide = clone.getElementsByClassName("blocklyScrollbarBackground");
for (var i = 0; i < toHide.length; i++) {
toHide[i].setAttribute("visibility", "hidden")
}
toHide = clone.querySelectorAll('image');
for (var i = 0; i < toHide.length; i++) {
toHide[i].setAttribute("visibility", "hidden")
}
toHide = clone.querySelectorAll('.blocklyMainBackground');
for (var i = 0; i < toHide.length; i++) {
toHide[i].parentElement.removeChild(toHide[i])
}
var zelement = clone.getElementById("rectCorner");
if (zelement) {
zelement.setAttribute("visibility", "hidden")
}
zelement = clone.getElementById("indicatorWarning");
if (zelement) {
zelement.setAttribute("visibility", "hidden")
}
var svg = doctype + outer.innerHTML;
svg = svg.replace(/ /g, ' ');
svg = svg.replace(/sans-serif/g, 'Arial, Verdana, "Nimbus Sans L", Helvetica');
var uri = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svg)));
if (cb) {
cb(uri)
}
}
function makeCRCTable() {
var c;
var crcTable = [];
for (var n = 0; n < 256; n++) {
c = n;
for (var k = 0; k < 8; k++) {
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1))
}
crcTable[n] = c
}
return crcTable
}
function crc32(data) {
var crcTable = window.crcTable || (window.crcTable = makeCRCTable());
var crc = 0 ^ (-1);
for (var i = 0; i < data.length; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ data[i]) & 0xFF]
}
return (crc ^ (-1)) >>> 0
}
var CODE_PNG_CHUNK = 'coDe';
function PNG() {
this.chunks = null
}
PNG.HEADER = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
var pHY_data = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
PNG.Chunk = function(length, type, data, crc) {
this.length = length;
this.type = type;
this.data = data;
this.crc = crc
};
PNG.prototype.readFromBlob = function(blob, callback) {
var reader = new FileReader();
var png = this;
reader.addEventListener('loadend', function() {
png.processData_(new Uint8Array(reader.result));
if (callback instanceof Function) callback(png)
});
reader.readAsArrayBuffer(blob)
};
PNG.prototype.getCodeChunk = function() {
if (!this.chunks) return null;
for (var i = 0; i < this.chunks.length; i++) {
if (this.chunks[i].type === CODE_PNG_CHUNK) {
return this.chunks[i]
}
}
return null
};
PNG.prototype.processData_ = function(data) {
var chunkStart = PNG.HEADER.length;
function decode4() {
var num;
num = data[chunkStart++];
num = num * 256 + data[chunkStart++];
num = num * 256 + data[chunkStart++];
num = num * 256 + data[chunkStart++];
return num
}
function read4() {
var str = '';
for (var i = 0; i < 4; i++, chunkStart++) {
str += String.fromCharCode(data[chunkStart])
}
return str
}
function readData(length) {
return data.slice(chunkStart, chunkStart + length)
}
this.chunks = [];
while (chunkStart < data.length) {
var length = decode4();
var type = read4();
var chunkData = readData(length);
chunkStart += length;
var crc = decode4();
this.chunks.push(new PNG.Chunk(length, type, chunkData, crc))
}
};
PNG.prototype.setCodeChunk = function(code) {
var text = new TextEncoder().encode(CODE_PNG_CHUNK + code);
var length = text.length - 4;
var crc = crc32(text);
text = text.slice(4);
for (var i = 0, chunk;
(chunk = this.chunks[i]); i++) {
if (chunk.type === CODE_PNG_CHUNK) {
chunk.length = length;
chunk.data = text;
chunk.crc = crc;
return
}
}
chunk = new PNG.Chunk(length, CODE_PNG_CHUNK, text, crc);
this.chunks.splice(this.chunks.length - 1, 0, chunk)
};
PNG.prototype.toBlob = function() {
var length = PNG.HEADER.length;
this.chunks.forEach(function(chunk) {
length += chunk.length + 12
});
var buffer = new Uint8Array(length);
var index = 0;
function write4(value) {
if (typeof value === 'string') {
var text = new TextEncoder().encode(value);
buffer.set(text, index);
index += text.length
} else {
buffer[index + 3] = value & 0xFF;
value >>= 8;
buffer[index + 2] = value & 0xFF;
value >>= 8;
buffer[index + 1] = value & 0xFF;
value >>= 8;
buffer[index] = value & 0xFF;
index += 4
}
}
function writeData(data) {
buffer.set(data, index);
index += data.length
}
writeData(PNG.HEADER);
this.chunks.forEach(function(chunk) {
write4(chunk.length);
write4(chunk.type);
writeData(chunk.data);
write4(chunk.crc)
});
return new Blob([buffer], {
'type': 'image/png'
})
};
function exportBlockAsPng(block) {
var xml = document.createElement('xml');
xml.appendChild(Blockly.Xml.blockToDom(block, true));
var code = Blockly.Xml.domToText(xml);
svgAsDataUri(block.svgGroup_, block.workspace.getMetrics(), null, function(uri) {
var img = new Image();
img.src = uri;
img.onload = function() {
var canvas = document.createElement('canvas');
canvas.width = 2 * img.width;
canvas.height = 2 * img.height;
var context = canvas.getContext('2d');
context.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
function download(png) {
png.setCodeChunk(code);
for (var i = 0; i < png.chunks.length; i++) {
var phy = [112, 72, 89, 115];
if (png.chunks[i].type == 'pHYs') {
png.chunks.splice(i, 1, new PNG.Chunk(9, 'pHYs', pHY_data, crc32(phy.concat(pHY_data))));
break
} else if (png.chunks[i].type == 'IDAT') {
png.chunks.splice(i, 0, new PNG.Chunk(9, 'pHYs', pHY_data, crc32(phy.concat(pHY_data))));
break
}
}
var blob = png.toBlob();
var a = document.createElement('a');
a.download = simpleString(block) + '.png';
a.target = '_self';
a.href = URL.createObjectURL(blob);
document.body.appendChild(a);
a.addEventListener("click", function(e) {
a.parentNode.removeChild(a)
});
a.click()
}
if (canvas.toBlob === undefined) {
var src = canvas.toDataURL('image/png');
var base64img = src.split(',')[1];
var decoded = window.atob(base64img);
var rawLength = decoded.length;
var buffer = new Uint8Array(new ArrayBuffer(rawLength));
for (var i = 0; i < rawLength; i++) {
buffer[i] = decoded.charCodeAt(i)
}
var blob = new Blob([buffer], {
'type': 'image/png'
});
new PNG().readFromBlob(blob, download)
} else {
canvas.toBlob(function(blob) {
new PNG().readFromBlob(blob, download)
})
}
}
})
};
}, 20000);
})();