NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Gandi 手机版
// @name:en Gandi Mobile support
// @namespace http://tampermonkey.net/
// @version 2025-2-9-2
// @description 优化 Gandi 编辑器在手机端的体验
// @description:en Optimize the Gandi editor experience on mobile
// @author 白猫
// @match https://www.ccw.site/gandi/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=ccw.site
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const eyesSVG = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><defs><clipPath id="master_svg0_2_4"><rect x="0" y="0" width="24" height="24" rx="0"></rect></clipPath></defs><g clip-path="url(#master_svg0_2_4)"><g><path d="M12.075146484375,5.25Q4.875146484375,5.25,0.525146484375,12.6Q5.550146484375,19.725,12.075146484375,19.725Q18.600146484375,19.725,23.625146484375,12.6Q19.275146484375,5.25,12.075146484375,5.25ZM19.500146484375,15.3C17.100146484375,17.325,14.550146484375,18.6,12.000146484375,18.6C9.450146484375,18.6,6.900146484375,17.4,4.500146484375,15.3C3.675146484375,14.55,2.850146484375,13.8,2.175146484375,12.975C2.025146484375,12.825,1.950146484375,12.675,1.800146484375,12.525C1.875146484375,12.375,2.025146484375,12.225,2.100146484375,12.075C2.700146484375,11.25,3.450146484375,10.425,4.275146484375,9.675C6.600146484375,7.575,9.150146484375,6.3,12.000146484375,6.3C14.850146484375,6.3,17.400146484375,7.575,19.725146484375,9.675C20.550146484375,10.425,21.225146484375,11.25,21.900146484375,12.075C21.975146484375,12.225,22.125146484375,12.375,22.200146484375,12.525C22.125146484375,12.675,21.975146484375,12.825,21.825146484375,12.975C21.150146484375,13.725,20.400146484375,14.55,19.500146484375,15.3Z" fill="#000000" fill-opacity="0.3400000035762787" style="mix-blend-mode:passthrough"></path></g><g><path d="M12.000146484375,8.10003662109375C9.750146484375,8.10003662109375,8.025146484375,9.90003662109375,8.025146484375,12.07503662109375C8.025146484375,14.32503662109375,9.825146484375,16.05003662109375,12.000146484375,16.05003662109375C14.250146484375,16.05003662109375,15.975146484375,14.25003662109375,15.975146484375,12.07503662109375C15.975146484375,9.90003662109375,14.175146484375,8.10003662109375,12.000146484375,8.10003662109375ZM12.000146484375,15.00003662109375C10.425146484375,15.00003662109375,9.150146484375,13.72503662109375,9.150146484375,12.15003662109375C9.150146484375,10.57503662109375,10.425146484375,9.30003662109375,12.000146484375,9.30003662109375C13.575146484375,9.30003662109375,14.850146484375,10.57503662109375,14.850146484375,12.15003662109375C14.850146484375,13.65003662109375,13.575146484375,15.00003662109375,12.000146484375,15.00003662109375Z" fill="#000000" fill-opacity="0.3400000035762787" style="mix-blend-mode:passthrough"></path></g></g></svg>';
//加上第三方devtool
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/eruda';
script.onload = function () {
eruda.init();
var devtoolStyle = document.createElement("style");
devtoolStyle.innerHTML = `
.luna-console-log-content {
display: flex !important;
flex-direction: column !important;
}
`;
document.querySelector('#eruda')?.shadowRoot.appendChild(devtoolStyle);
const text = `
/$$ /$$ /$$ /$$ /$$$$$$ /$$ /$$
| $$ | $$ | $$| $$ /$$__ $$ | $$|__/
| $$ | $$ /$$$$$$ | $$| $$ /$$$$$$ | $$ \\__/ /$$$$$$ /$$$$$$$ /$$$$$$$ /$$
| $$$$$$$$ /$$__ $$| $$| $$ /$$__ $$ | $$ /$$$$ |____ $$| $$__ $$ /$$__ $$| $$
| $$__ $$| $$$$$$$$| $$| $$| $$ \\ $$ | $$|_ $$ /$$$$$$$| $$ \\ $$| $$ | $$| $$
| $$ | $$| $$_____/| $$| $$| $$ | $$ | $$ \\ $$ /$$__ $$| $$ | $$| $$ | $$| $$
| $$ | $$| $$$$$$$| $$| $$| $$$$$$/ | $$$$$$/| $$$$$$$| $$ | $$| $$$$$$$| $$
|__/ |__/ \\_______/|__/|__/ \\______/ \\______/ \\_______/|__/ |__/ \\_______/|__/
`;
function generateBlueToWhiteGradient(n) {
const colors = [];
for (let i = 0; i < n; i++) {
const lightness = 30 + (i / (n - 1)) * 70; // 亮度从30%增加到100%
colors.push(`color: hsl(240, 100%, ${lightness}%);`); // HSL中的240是纯蓝色
}
return colors;
}
const colors = generateBlueToWhiteGradient(10);
// 按行分割
const lines = text.split("\n");
// 生成格式化字符串
let logString = "";
let styles = [];
lines.forEach((line, index) => {
if (line.trim() !== "") { // 过滤掉空行
logString += `%c${line}\n`;
styles.push(colors[index % colors.length]); // 让颜色循环使用
}
});
// 统一输出,不会被 `console.log` 自动分割
console.log(logString, ...styles);
console.log('欢迎使用 Gandi 手机版 插件,让您的移动设备编程体验更上一层楼!');
console.log('有任何问题可以加Q群:760188536 与我们一起交流!');
};
document.body.appendChild(script);
//计算屏幕缩放比例
function pluginCalculateScale() {
const bodyHeight = document.body.clientHeight;
const a = 0.00219;
const b = -0.13146;
const scale = a * bodyHeight + b;
return Math.min(scale, 1); // 确保结果不超过 1
}
function pageCalculateScale() {
const screenHeight = window.innerHeight;
const a = 0.00161;
const b = -0.091;
return Math.min(1, a * screenHeight + b);
}
let pluginScale = pluginCalculateScale(),pageScale = pageCalculateScale(); //计算插件缩放
document.documentElement.style.setProperty('--plugin-scale', pluginScale);
document.documentElement.style.setProperty('--page-scale', pageScale);
window.addEventListener('resize', () => {
pluginScale = pluginCalculateScale();
pageScale = pageCalculateScale();
document.documentElement.style.setProperty('--plugin-scale', pluginScale);
document.documentElement.style.setProperty('--page-scale', pageScale);
});
//禁用自动放大
var meta = document.createElement("meta");
meta.name = "viewport";
meta.content = "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no";
document.head.appendChild(meta);
// 修改所有输入框的样式,防止自动放大
var style = document.createElement("style");
style.innerHTML = `
input, textarea {
font-size: 16px !important; /* 避免 iOS 自动放大 */
touch-action: manipulation; /* 禁用双击放大 */
}
.react-draggable {
transform-origin: left top;
}
.MuiDialogTitle-root {
padding: 1vh !important;
}
.MuiDialogActions-root {
padding: 1vh !important;
}
.close-button-11FHp {
padding: 0px !important;
}
.title-3Gkc- {
margin: 0px !important;
}
.MuiDialogContent-root {
margin: 0px !important;
}
.gandi_setting-modal_scroller_2rlIe {
zoom: var(--page-scale) !important;
}
.gandi_bulletin-modal_modal-overlay_TBAhj {
zoom: var(--page-scale) !important;
}
.css-8ipe1d {
zoom: var(--page-scale) !important;
height: 100% !important;
}
.gandi_setting-modal_menu_2IwVr {
overflow: scroll;
zoom: var(--page-scale) !important;
}
`;
document.head.appendChild(style);
function ToolBar() {
const scratchCategoryMenu = document.querySelector('.scratchCategoryMenu');
const toolboxSwitchButton = document.querySelector('.toolboxSwitchButton');
if (scratchCategoryMenu && toolboxSwitchButton) {
let touchTimer = null;
scratchCategoryMenu.addEventListener('touchstart', (event) => {
touchTimer = setTimeout(() => {
touchTimer = null; // 超时不触发
}, 200);
});
scratchCategoryMenu.addEventListener('touchend', (event) => {
if (touchTimer) {
clearTimeout(touchTimer);
touchTimer = null;
const parent = toolboxSwitchButton?.parentElement?.parentElement;
if (parent && parent.classList.contains('collapsed')) {
toolboxSwitchButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if ([...mutation.addedNodes].some(node => node.classList?.contains('blocklyInsertionMarker'))) {
toolboxSwitchButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
observer.disconnect();
break;
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
});
scratchCategoryMenu.addEventListener('touchmove', () => {
clearTimeout(touchTimer);
touchTimer = null; // 移动手指则取消触发
});
scratchCategoryMenu.addEventListener('touchcancel', () => {
clearTimeout(touchTimer);
touchTimer = null; // 取消触摸时也不触发
});
}
}
function waitForElement(selector, callback) {
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect(); // 先停止观察,等待元素移除
waitForRemoval(element, ()=>{
const toolboxSwitchButton = document.querySelector('.toolboxSwitchButton');
const parent = toolboxSwitchButton?.parentElement?.parentElement;
if (parent && !parent.classList.contains('collapsed')) {
toolboxSwitchButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
}
setTimeout(() => ToolBar(), 2000);
//天杀的scratch切换角色怎么还移除积木栏啊啊啊啊
let wasRemoved = false; // 记录积木区是否曾被删除
const ToolBarObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
mutation.removedNodes.forEach(node => {
if (node.classList?.contains('scratchCategoryMenu')) {
wasRemoved = true;
}
});
mutation.addedNodes.forEach(node => {
if (node.classList?.contains('scratchCategoryMenu') && wasRemoved) {
wasRemoved = false;
ToolBar();
}
});
}
});
ToolBarObserver.observe(document.body, { childList: true, subtree: true });
//注入插件区域,兼容小屏手机
const pluginsRoot = document.querySelector('.gandi_plugins_plugins-root_xA3t3');
if (pluginsRoot) {
// 设置样式
Object.assign(pluginsRoot.style, {
height: 'calc(100% - 128px)',
overflowY: 'scroll',
width: '36px',
left: '-43px',
});
// 遍历所有一级子节点并添加缩放
pluginsRoot.childNodes.forEach(child => {
if (child.nodeType === 1) { // 确保是元素节点
child.style.zoom = '0.7';
}
});
}
});
callback();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function waitForRemoval(element, callback) {
const observer = new MutationObserver(() => {
if (!document.body.contains(element)) {
observer.disconnect();
callback();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
waitForElement('.gandi_loader_background_2DPrW', () => {
init();
});
function init() {
function hideScrollbar(domElement) {
if (domElement) {
// 适用于一般浏览器
domElement.style.scrollbarWidth = 'none';
domElement.style.msOverflowStyle = 'none';
domElement.classList.add('hide-scrollbar');
// 适用于 WebKit(Chrome、Safari)
const style = document.createElement('style');
style.textContent = `
${domElement.className}::-webkit-scrollbar {
display: none;
}
`;
document.head.appendChild(style);
}
}
hideScrollbar(document.querySelector('.gandi_target-pane_target-list_10PNw'))
hideScrollbar(document.querySelector('.gandi_plugins_plugins-root_xA3t3'));
document.querySelector('.gandi_stage-header_stage-menu-wrapper_15JJt')?.style.setProperty('height', '100%', 'important');
document.querySelector('.gandi_editor-wrapper_tabPanelWrapper_Fb3KY')?.style.setProperty('overflow', 'scroll', 'important');
const StageBar = document.querySelector('.xg-stage-menu-wrapper');
StageBar?.style.setProperty('min-height', '0px', 'important');
StageBar?.style.setProperty('max-height', '60px', 'important');
StageBar?.style.setProperty('flex', '1', 'important');
StageBar?.parentElement?.style.setProperty('height', '100%', 'important');
StageBar?.parentElement?.style.setProperty('display', 'flex', 'important');
StageBar?.parentElement?.style.setProperty('flex-direction', 'column', 'important');
const verticalBar = document.querySelector('.gandi_vertical-bar_bar_Tsvpu');
hideScrollbar(verticalBar);
verticalBar?.style.setProperty('height', 'calc(100vh - 60px)', 'important');
verticalBar?.style.setProperty('overflow-y', 'scroll', 'important');
//针对画板和声音界面的优化
document.querySelectorAll('.gandi_editor-wrapper_tab_2OPuA').forEach((tab, index) => {
tab.addEventListener('click', () => {
const sideBar = document.querySelector('.gandi_vertical-bar_bar_Tsvpu');
const pluginsRoot = document.querySelector('.gandi_plugins_plugins-root_xA3t3');
if (!sideBar || !pluginsRoot) return; // 防止报错
if (index === 0) {
// **第一个Tab被点击**
sideBar.style.width = '72px';
sideBar.style.padding = '12px 11px';
sideBar.style.borderRight = '1px solid var(--theme-color-200)';
pluginsRoot.style.display = 'flex';
} else if (index === 1 || index === 2) {
// **第二个或第三个Tab被点击**
sideBar.style.width = '0';
sideBar.style.padding = '0';
sideBar.style.borderRight = '0px';
pluginsRoot.style.display = 'none';
}
});
});
let collapsibleBoxes = document.querySelectorAll('.gandi_collapsible-box_collapsible-box_1_329');
if (collapsibleBoxes.length > 1) {
collapsibleBoxes[0].style.top = '10px';
collapsibleBoxes[0].style.height = 'calc(100% - 15px)';
collapsibleBoxes[0].style.transition = 'all 0.2s ease-out';
collapsibleBoxes[0].style.transition = 'all 0.2s ease-out';
collapsibleBoxes[1].style.top = '10px';
collapsibleBoxes[1].style.height = 'calc(100% - 15px)';
collapsibleBoxes[1].style.transition = 'all 0.2s ease-out';
collapsibleBoxes[1].lastChild.style.overflow = 'hidden';
}
let switchButtons = document.querySelectorAll('.gandi_collapsible-box_switch-button_2A5kM');
switchButtons.forEach(button => {
let parent = button.parentElement?.parentElement;
if (parent && !parent.classList.contains('gandi_collapsible-box_collapsed_oQuU1')) {
setTimeout(() => button.click(), 100);
}
});
let headers = document.querySelectorAll('.gandi_collapsible-box_header_dc9Es');
if (headers.length > 1) {
let newButton = document.createElement('span');
newButton.className = 'gandi_collapsible-box_switch-button_2A5kM';
newButton.setAttribute('data-xg_idx', '4');
newButton.innerHTML = eyesSVG;
newButton.addEventListener('click', () => {
let target = collapsibleBoxes[1];
if(target.style.height === '28px'){
if (target) {
target.style.height = 'calc(100% - 15px)';
}
let addScript = document.querySelectorAll('.gandi_action-menu_menu-container_3a6da');
addScript = addScript[addScript.length - 1];
if(addScript) {
addScript.style.transform = 'unset';
}
}else{
if (target) {
target.style.height = '28px';
}
let addScript = document.querySelectorAll('.gandi_action-menu_menu-container_3a6da');
addScript = addScript[addScript.length - 1];
if(addScript) {
addScript.style.transform = 'scale(0)';
}
}
});
headers[1].appendChild(newButton);
}
function hideStage() {
const firstButton = document.querySelectorAll('.gandi_collapsible-box_switch-button_2A5kM');
const parent = firstButton[0]?.parentElement?.parentElement;
const parents = firstButton[1]?.parentElement?.parentElement;
if (parent && parents && (parent.classList.contains('gandi_collapsible-box_collapsed_oQuU1') === parents.classList.contains('gandi_collapsible-box_collapsed_oQuU1'))) {
firstButton[0]?.click();
}
}
function monitorFirstSwitchButton() {
const firstElement = document.querySelectorAll('.gandi_collapsible-box_switch-button_2A5kM');
if (firstElement[1]) {
firstElement[1].addEventListener('click', () => {
hideStage();
});
}
}
monitorFirstSwitchButton();
//删除影响点击的元素(我不知道为什么sb Gandi插件一旦被关闭窗口马上创建一个覆盖全页面的寄吧元素不让点击)
const blackBard = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.classList.contains('addons_interlayer_lVD80')) {
node.remove();
}
});
}
});
blackBard.observe(document.body, { childList: true, subtree: true });
//将所有的插件页面缩小
const plugin = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const target = mutation.target;
if (target.classList.contains('react-draggable')) {
const transform = target.style.transform || '';
const scaleRegex = /scale\(?[^)]+\)/;
const newScale = `scale(${pluginScale})`;
if (scaleRegex.test(transform)) {
target.style.transform = transform.replace(scaleRegex, newScale);
} else {
target.style.transform = `${transform} ${newScale}`;
}
}
}
});
});
// 监听所有 react-draggable 元素
const observeDraggableElements = () => {
document.querySelectorAll('.react-draggable').forEach(el => {
const transform = el.style.transform || '';
const scaleRegex = /scale\(?[^)]+\)/;
const newScale = `scale(${pluginScale})`;
if (scaleRegex.test(transform)) {
el.style.transform = transform.replace(scaleRegex, newScale);
} else {
el.style.transform = `${transform} ${newScale}`;
}
plugin.observe(el, { attributes: true, attributeFilter: ['style'] });
});
};
// 初次运行
observeDraggableElements();
// 监听新元素的插入,确保后续添加的 react-draggable 也被监听
const domObserver = new MutationObserver(() => observeDraggableElements());
domObserver.observe(document.body, { childList: true, subtree: true });
//监听代码编辑器
const codeEditorObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
const target = document.querySelector('#codeEditor')?.firstElementChild?.firstElementChild;
if (target) {
target.style.width = '10vw';
}
}
});
codeEditorObserver.observe(document.body, { childList: true, subtree: true });
}
})();