basic stuff

This commit is contained in:
JeremyGamer13 2023-07-29 18:38:01 -06:00
commit 4abadd5358
28 changed files with 4542 additions and 1 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
engine-strict=true
resolution-mode=highest

View file

@ -1,7 +1,14 @@
<img src="https://raw.githubusercontent.com/JeremyGamer13/turbobuilder/main/icon.png"/>
<img src="./icon.png" width="64" height="64" /> <img src="./icon_title.png" height="64" />
# TurboBuilder
Create extensions for TurboWarp using block-based coding.
## In development
This project is not finished and is still being worked on. Expect bugs and problems that may prevent the site from working.
## Running locally
1. Clone the repo
2. Run `npm i --force` in a terminal inside the folder where the repo is
3. Run `npm run dev` in a terminal inside the folder where the repo is
4. Visit http://localhost:5173/

BIN
icon_nomargin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
icon_title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

3723
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "turbobuilder",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"svelte": "^4.0.5",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@blockly/continuous-toolbox": "^5.0.2",
"@sveltejs/adapter-vercel": "^3.0.2",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"svelte-blockly": "^0.1.0"
}
}

48
src/app.html Normal file
View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<title>TurboBuilder - Make extensions with blocks</title>
<meta name="description" content="Create TurboWarp extensions using Scratch-like block coding." />
<meta name="keywords" content="turbowarp, extensions, blocks" />
<meta name="author" content="JeremyGamer13" />
<meta name="theme-color" content="#ff4b4b" />
<meta name="og:image" content="icon.png" />
<meta name="viewport" content="width=device-width" />
<style>
*:not(code) {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
a {
color: #ff4b4b;
}
a:hover {
color: #ff7373;
}
a:active {
color: #a52b2b;
}
/* blockly overrides */
.blocklyMenu {
color: black;
}
.blocklyToolboxCategory {
cursor: pointer;
}
.blocklyTreeLabel {
font-size: 10px !important;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.categoryBubble {
border: 1px rgba(0,0,0,0.35) solid;
}
</style>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,9 @@
<div class="divider" />
<style>
.divider {
border-right: 1px dashed rgba(0, 0, 0, 0.15);
margin: 0 0.5rem;
height: 90%;
}
</style>

View file

@ -0,0 +1,40 @@
<script>
// Components
import Divider from "$lib/NavigationBar/Divider.svelte";
</script>
<div class="nav">
<img
src="/images/icon.png"
alt="Logo"
class="logo-margin"
style="height: 100%;"
/>
<Divider />
<slot />
</div>
<style>
:root {
--nav-height: 3rem;
}
.nav {
position: fixed;
left: 0px;
top: 0px;
width: calc(100% - 8px);
height: calc(var(--nav-height) - 8px);
padding: 4px;
display: flex;
flex-direction: row;
align-items: center;
background: #ff4b4b;
}
.logo-margin {
margin-right: 8px;
}
</style>

View file

@ -0,0 +1,6 @@
<xml>
<category name="Control" colour="#FFAB19">
<block type="control_ifthen" />
<block type="control_ifthenelse" />
</category>
</xml>

1
src/lib/index.js Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -0,0 +1,72 @@
import javascriptGenerator from '../javascriptGenerator';
import registerBlock from '../register';
const categoryPrefix = 'control_';
const categoryColor = '#FFAB19';
function register() {
// if <> then {}
registerBlock(`${categoryPrefix}ifthen`, {
message0: 'if %1 then %2 %3',
args0: [
{
"type": "input_value",
"name": "CONDITION",
"check": "Boolean"
},
{
"type": "input_dummy"
},
{
"type": "input_statement",
"name": "BLOCKS"
}
],
previousStatement: null,
nextStatement: null,
inputsInline: true,
colour: categoryColor
}, (block) => {
const CONDITION = javascriptGenerator.valueToCode(block, 'CONDITION', javascriptGenerator.ORDER_ATOMIC);
const BLOCKS = javascriptGenerator.statementToCode(block, 'BLOCKS');
const code = `if (${CONDITION ? `Boolean(${CONDITION})` : 'false'}) { ${BLOCKS} };`;
return `${code}\n`;
})
// if <> then {} else {}
registerBlock(`${categoryPrefix}ifthenelse`, {
message0: 'if %1 then %2 %3 else %4 %5',
args0: [
{
"type": "input_value",
"name": "CONDITION",
"check": "Boolean"
},
{
"type": "input_dummy"
},
{
"type": "input_statement",
"name": "BLOCKS"
},
{
"type": "input_dummy"
},
{
"type": "input_statement",
"name": "BLOCKS2"
}
],
previousStatement: null,
nextStatement: null,
inputsInline: true,
colour: categoryColor
}, (block) => {
const CONDITION = javascriptGenerator.valueToCode(block, 'CONDITION', javascriptGenerator.ORDER_ATOMIC);
const BLOCKS = javascriptGenerator.statementToCode(block, 'BLOCKS');
const BLOCKS2 = javascriptGenerator.statementToCode(block, 'BLOCKS2');
const code = `if (${CONDITION ? `Boolean(${CONDITION})` : 'false'}) { ${BLOCKS} } else { ${BLOCKS2} };`;
return `${code}\n`;
})
}
export default register;

View file

@ -0,0 +1,11 @@
import javascriptGenerator from '../javascriptGenerator';
import registerBlock from '../register';
const categoryPrefix = 'core_';
const categoryColor = '#ff4b4b';
function register() {
}
export default register;

View file

@ -0,0 +1,67 @@
import javascriptGenerator from '../javascriptGenerator';
import registerBlock from '../register';
const categoryPrefix = 'generic_';
const categoryColor = '#fff';
function register() {
// number
registerBlock(`${categoryPrefix}number`, {
message0: '%1',
args0: [
{
"type": "field_number",
"name": "NUMBER",
"value": 0
}
],
output: "Number",
inputsInline: true,
colour: categoryColor
}, (block) => {
const NUMBER = block.getFieldValue('NUMBER');
const code = `Number(${NUMBER})`;
return [code, javascriptGenerator.ORDER_NONE];
})
// text
registerBlock(`${categoryPrefix}text`, {
message0: '%1',
args0: [
{
"type": "field_input",
"name": "TEXT",
"text": ""
}
],
output: "String",
inputsInline: true,
colour: categoryColor
}, (block) => {
const TEXT = block.getFieldValue('TEXT');
const code = `String(${JSON.stringify(TEXT)})`;
return [code, javascriptGenerator.ORDER_NONE];
})
// boolean
registerBlock(`${categoryPrefix}boolean`, {
message0: '%1',
args0: [
{
"type": "field_dropdown",
"name": "STATE",
"options": [
["True", "true"],
["False", "false"],
["Random", "Boolean(Math.round(Math.random()))"]
]
}
],
output: "Boolean",
inputsInline: true,
colour: categoryColor
}, (block) => {
const code = block.getFieldValue('STATE');
return [code, javascriptGenerator.ORDER_NONE];
})
}
export default register;

View file

@ -0,0 +1,18 @@
const throwAwayVars = {}; // used for repeat loops
const compileVars = {};
compileVars._idx = 0;
compileVars.new = () => {
const _listLow = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
const _listHigh = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
const _listSym = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '@', '#', '$', '%', '&', '(', ')', '_', '-', '+', '=', '[', ']', '|'];
const list = [].concat(_listLow, _listHigh, _listSym);
let str = '';
for (let i = 0; i < 100; i++) {
str += list[Math.round(Math.random() * (list.length - 1))];
};
return str;
};
compileVars.next = () => {
compileVars._idx++;
return `v${compileVars._idx}`;
};

View file

@ -0,0 +1,35 @@
// compile functions
import raw_randomNumberGen from './randomNumberGen.js?raw';
import raw_compileVarSection from './compileVarSection.js?raw';
import javascriptGenerator from '../javascriptGenerator';
class Compiler {
/**
* Generates JavaScript code from the provided workspace.
* @param {Blockly.Workspace} workspace
* @returns {string} Generated code.
*/
compile(workspace) {
const code = javascriptGenerator.workspaceToCode(workspace);
const headerCode = [
`/*`,
` This extension was made with TurboBuilder!`,
` https://turbobuilder.vercel.app/`,
`*/`,
`(function (Scratch) {`,
`const variables = {};`,
raw_compileVarSection,
raw_randomNumberGen
];
const footerCode = [
`Scratch.extensions.register(new Extension());`,
`})(Scratch);`
];
return [].concat(headerCode, code, footerCode).join('\n');
}
}
export default Compiler;

View file

@ -0,0 +1,18 @@
/**
* Chooses a random number between the min and max.
* @param {number} min
* @param {number} max
* @returns {number}
*/
function randomNumberGen(min, max) {
// swap if min is larger
if (min > max) {
let _v = max;
max = min;
min = _v;
}
// math
const difference = max - min;
const random = Math.random() * difference;
return min + random;
};

View file

@ -0,0 +1,37 @@
import Blockly from "blockly/core";
import javascriptGenerator from '../javascriptGenerator';
function xmlToCode(xml) {
// this sucks but i dont know any other method
// make div
const tempDiv = document.createElement("div");
tempDiv.style = "display:none";
document.body.append(tempDiv);
// inject workpace
const workspace = Blockly.inject(tempDiv, {
collapse: true,
comments: true,
scrollbars: true,
disable: false
});
let code = "";
try {
const dom = Blockly.utils.xml.textToDom(xml);
Blockly.Xml.domToWorkspace(dom, workspace);
// yay we get to compile now
code = javascriptGenerator.workspaceToCode(workspace);
} catch (err) {
// we do try catch so if we fail to parse
// we dont leave behind an entire workspace & div in the document
console.warn("could not compile xml;", err);
}
// gtfo
workspace.dispose();
tempDiv.remove();
return code;
}
export default xmlToCode;

View file

@ -0,0 +1,37 @@
// file-dialog exists on NPM and thats what this file is
// however it uses module.exports and exports a function
// which vite absolutely HATES and REFUSES to build no matter what
// so its reimplemented here with a few changes to work
function fileDialog(...args) {
const input = document.createElement('input');
// Set config
if (typeof args[0] === 'object') {
if (args[0].multiple === true) input.setAttribute('multiple', '');
if (args[0].accept !== undefined) input.setAttribute('accept', args[0].accept);
}
input.setAttribute('type', 'file');
// IE10/11 Addition
input.style.display = 'none';
input.setAttribute('id', 'hidden-file');
document.body.appendChild(input);
// Return promise/callvack
return new Promise(resolve => {
input.addEventListener('change', () => {
resolve(input.files);
const lastArg = args[args.length - 1];
if (typeof lastArg === "function") lastArg(input.files);
// IE10/11 Addition
document.body.removeChild(input);
})
// Simluate click event
input.click();
})
}
export default fileDialog;

View file

@ -0,0 +1,10 @@
// vercel's build doesnt work for some reason
// its related to js generator and how its imported
// so lets just import it the way that it wants
// we COULD modify the javascript generator here
// but its much cleaner to leave this alone
import pkg from 'blockly/javascript.js';
const { javascriptGenerator } = pkg;
export default javascriptGenerator;

View file

@ -0,0 +1,13 @@
/**
* Preloads all audio files specified.
* This is because the hosted version of TurboBuilder will cause a bit of a delay before playing audio
* due to the host having to provide the file, not the local machine.
* @param {Array} files An array full of file paths to audio files.
*/
function preload(files) {
for (const path of files) {
new Audio(path);
}
}
export default preload;

View file

@ -0,0 +1,16 @@
import Blockly from 'blockly/core';
import javascriptGenerator from '../javascriptGenerator';
export default (blockName, jsonData, compileFunction) => {
const blockObject = {
init: function () {
this.jsonInit(jsonData);
}
};
// register visual block
Blockly.Blocks[blockName] = blockObject
// register block compile function
javascriptGenerator[blockName] = compileFunction;
}

312
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,312 @@
<script>
import { onMount } from "svelte";
// Components
import NavigationBar from "$lib/NavigationBar/NavigationBar.svelte";
// Toolbox
import Toolbox from "$lib/Toolbox/Toolbox.xml?raw";
import JSZip from "jszip";
import * as FileSaver from "file-saver";
import fileDialog from "../resources/fileDialog";
import Blockly from "blockly/core";
import * as ContinuousToolboxPlugin from "@blockly/continuous-toolbox";
const Theme = Blockly.Theme.defineTheme("BasicTheme", {
base: Blockly.Themes.Classic,
fontStyle: {
family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
weight: "600",
size: 12,
},
startHats: true,
});
import En from "blockly/msg/en";
import "blockly/blocks";
import BlocklyComponent from "svelte-blockly";
import Compiler from "../resources/compiler";
import preload from "../resources/preload";
// Blocks
import registerGeneric from "../resources/blocks/generic.js";
registerGeneric();
import registerCore from "../resources/blocks/core.js";
import registerControl from "../resources/blocks/control.js";
registerCore();
registerControl();
const en = {
rtl: false,
msg: {
...En,
},
};
const config = {
toolbox: Toolbox,
collapse: true,
comments: true,
scrollbars: true,
disable: false,
theme: Theme,
renderer: "zelos",
zoom: {
controls: true,
wheel: true,
startScale: 0.8,
maxScale: 4,
minScale: 0.25,
scaleSpeed: 1.1,
},
plugins: {
toolbox: ContinuousToolboxPlugin.ContinuousToolbox,
flyoutsVerticalToolbox: ContinuousToolboxPlugin.ContinuousFlyout,
metricsManager: ContinuousToolboxPlugin.ContinuousMetrics,
},
};
let workspace;
let compiler;
let lastGeneratedCode = "";
onMount(() => {
console.log("ignore the warnings above we dont care about those");
window.onbeforeunload = () => "";
compiler = new Compiler(workspace);
// workspace was changed
workspace.addChangeListener(() => {
const code = compiler.compile(workspace);
lastGeneratedCode = code;
});
});
let fileMenu;
function showFileMenu() {
if (fileMenu.style.display == "none") {
fileMenu.style.display = "";
return;
}
fileMenu.style.display = "none";
}
let projectName = "";
function downloadProject() {
// generate file name
let filteredProjectName = projectName.replace(/[^a-z0-9\-]+/gim, "_");
let fileName = filteredProjectName + ".tbext";
if (!filteredProjectName) {
fileName = "MyProject.tbext";
}
// data
const projectData = State.serializeProject(State.currentProject);
// zip
const zip = new JSZip();
zip.file(
"README.txt",
"This file is not meant to be opened!" +
"\nBe careful as you can permanently break your project!"
);
// workspaces
const workspaces = zip.folder("workspaces");
for (const character of State.currentProject.characters) {
workspaces.file(character.id + ".xml", character.xml);
}
// data
const data = zip.folder("data");
data.file("project.json", projectData);
// download
zip.generateAsync({ type: "blob" }).then((blob) => {
FileSaver.saveAs(blob, fileName);
});
}
function loadProject() {
fileDialog({ accept: ".tbext" }).then((files) => {
if (!files) return;
const file = files[0];
// set project name
const projectNameIdx = file.name.lastIndexOf(".tbext");
projectName = file.name.substring(0, projectNameIdx);
JSZip.loadAsync(file.arrayBuffer()).then(async (zip) => {
console.log("loaded zip file...");
// get project json from the data folder
const dataFolder = zip.folder("data");
const projectJsonString = await dataFolder
.file("project.json")
.async("string");
const projectJson = JSON.parse(projectJsonString);
// get project workspace xml stuffs
const workspacesFolder = zip.folder("workspaces");
const fileNames = [];
workspacesFolder.forEach((_, file) => {
const fileName = file.name.replace("workspaces/", "");
fileNames.push(fileName);
});
// console.log(fileNames); // debug
const idWorkspacePairs = {};
for (const fileName of fileNames) {
const idx = fileName.lastIndexOf(".xml");
const id = fileName.substring(0, idx);
// assign to pairs
idWorkspacePairs[id] = await workspacesFolder
.file(fileName)
.async("string");
}
// console.log(idWorkspacePairs); // debug
// laod
console.log(projectJson); // debug
State.loadProject(projectJson, idWorkspacePairs);
});
});
}
</script>
<NavigationBar>
<input
class="project-name"
type="text"
placeholder="Extension Name here"
style="margin-left:4px;margin-right:4px"
bind:value={projectName}
/>
</NavigationBar>
<div class="main">
<div class="row-menus">
<div class="blocklyWrapper">
<BlocklyComponent {config} locale={en} bind:workspace />
</div>
<div class="row-submenus">
<div class="assetsWrapper">
<h1>Assets</h1>
{#if projectName}
<p>{projectName} extension</p>
{:else}
<p>Extension</p>
{/if}
<p>
These things are not required, you can leave them empty if
you want!
</p>
<br />
<p>
Documentation URL:
<input type="text" placeholder="https://..." />
</p>
<p>
Extension Icon:
<input type="file" />
</p>
<!-- <p class="warning">
Warning! This is not an image! The icon may appear broken!
</p> -->
</div>
<div class="codeWrapper">
<textarea
value={lastGeneratedCode}
disabled="true"
style="width:100%;height:100%;border:0;padding:0;color:white;background:black;font-family:monospace"
/>
</div>
</div>
</div>
</div>
<style>
:root {
--nav-height: 3rem;
}
.main {
position: absolute;
left: 0px;
top: var(--nav-height);
width: 100%;
height: calc(100% - var(--nav-height));
}
.project-name {
width: 236px;
font-size: 20px;
border-radius: 6px;
outline: 1px dashed rgba(0, 0, 0, 0.15);
border: 0;
background: rgba(255, 255, 255, 0.25);
color: white;
font-weight: bold;
font-size: 1rem;
padding: 0.5rem;
transition: 0.25s;
}
.project-name::placeholder {
font-weight: normal;
color: white;
opacity: 1;
font-style: italic;
}
.project-name:hover {
background-color: hsla(0, 100%, 100%, 0.5);
transition: 0.25s;
}
.project-name:active,
.project-name:focus {
outline: none;
border: 1px solid hsla(0, 100%, 100%, 0);
box-shadow: 0 0 0 calc(0.5rem * 0.5) hsla(0, 100%, 100%, 0.25);
background-color: hsla(0, 100%, 100%, 1);
color: black;
transition: 0.25s;
}
.row-menus {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
overflow: hidden;
}
.row-submenus {
display: flex;
flex-direction: column;
width: 35%;
height: 100%;
}
.blocklyWrapper {
position: relative;
width: 65%;
height: 100%;
}
.assetsWrapper {
position: relative;
width: 100%;
height: 50%;
}
.codeWrapper {
position: relative;
width: 100%;
height: 50%;
}
.warning {
background-color: yellow;
color: black;
}
</style>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

13
svelte.config.js Normal file
View file

@ -0,0 +1,13 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

12
vite.config.js Normal file
View file

@ -0,0 +1,12 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
optimizeDeps: {
include: [
'@blockly/continuous-toolbox',
'file-saver',
]
}
});