Compare commits

...

7 Commits

Author SHA1 Message Date
c9d19538da Added collapse code editor button
All checks were successful
FTP Upload on Push / deploy (push) Successful in 8s
FTP Upload on Push / release (push) Successful in 7s
2025-12-31 18:47:41 +01:00
4ecdd3a419 added s.pal, s.palc and s.paint
All checks were successful
FTP Upload on Push / deploy (push) Successful in 8s
FTP Upload on Push / release (push) Successful in 6s
2025-12-31 10:37:41 +00:00
0d3d6d5abf Run button is now auto deselected, so pressing the spacebar no longer restarts the program
All checks were successful
FTP Upload on Push / deploy (push) Successful in 7s
FTP Upload on Push / release (push) Successful in 5s
2025-12-31 09:51:32 +00:00
bf7570055e Change to actual path, so 301 redirect doesnt occur and browser wont change method
All checks were successful
FTP Upload on Push / deploy (push) Successful in 5s
FTP Upload on Push / release (push) Successful in 5s
2025-12-26 19:36:42 +00:00
ab164256df Added code sharing into editor
All checks were successful
FTP Upload on Push / deploy (push) Successful in 8s
FTP Upload on Push / release (push) Successful in 7s
2025-12-26 19:00:34 +00:00
09d60b29b3 Added code storage api for storing code in the future
All checks were successful
FTP Upload on Push / deploy (push) Successful in 8s
FTP Upload on Push / release (push) Successful in 8s
2025-12-26 18:08:52 +00:00
175b6a19e0 Fix release action
All checks were successful
FTP Upload on Push / deploy (push) Successful in 7s
FTP Upload on Push / release (push) Successful in 9s
2025-12-26 16:03:05 +00:00
5 changed files with 515 additions and 4 deletions

View File

@@ -15,7 +15,7 @@ jobs:
- name: Upload specific files via FTP
shell: bash
run: |
FILES=("index.html" "screen.js")
FILES=("index.html" "screen.js" "codestorage/default.php")
for FILE in "${FILES[@]}"; do
echo "Uploading $FILE..."

View File

@@ -13,6 +13,6 @@ jobs:
uses: actions/checkout@v4
- name: Add screen.js to releases
uses: actions/gitea-release-action@v1
uses: https://git.haaxman.co.uk/actions/gitea-release-action@v1
with:
files: screen.js

93
codestorage/default.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
require_once 'secrets.php';
$host = DB_HOST;
$db_name = DB_NAME;
$username = DB_USER;
$password = DB_PASS;
try {
$pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$createTableSql = "
CREATE TABLE IF NOT EXISTS codeData (
id BIGINT PRIMARY KEY,
data LONGTEXT NOT NULL
) ENGINE=InnoDB;
";
$pdo->exec($createTableSql);
// 3. Handle Request Methods
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
// --- READ LOGIC ---
if (isset($_GET['id'])) {
$id = (int) $_GET['id'];
$stmt = $pdo->prepare("SELECT data FROM codeData WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
if ($row) {
header('Content-Type: text/plain');
echo $row['data'];
} else {
http_response_code(404);
echo "Error: Record with ID $id not found.";
}
} else {
http_response_code(400);
echo "Error: Missing 'id' parameter in query string.";
}
} elseif ($method === 'POST') {
// --- WRITE LOGIC ---
// Get raw POST body
$inputData = file_get_contents('php://input');
if (!empty($inputData)) {
$idGenerated = false;
$newId = 0;
// Generate a unique random ID and ensure it doesn't collide
while (!$idGenerated) {
$newId = mt_rand(100000, 999999999); // Range for the random ID
// Check if ID exists
$checkStmt = $pdo->prepare("SELECT id FROM codeData WHERE id = ?");
$checkStmt->execute([$newId]);
if (!$checkStmt->fetch()) {
$idGenerated = true;
}
}
// Insert the new row
$insertStmt = $pdo->prepare("INSERT INTO codeData (id, data) VALUES (?, ?)");
$insertStmt->execute([$newId, $inputData]);
header('Content-Type: application/json');
echo json_encode(['id' => $newId, 'status' => 'success']);
} else {
http_response_code(400);
echo "Error: POST body is empty.";
}
} else {
// Unsupported Method
http_response_code(405);
echo "Error: Method $method not allowed.";
}
} catch (PDOException $e) {
// Handle connection or query errors
http_response_code(500);
echo "Database Error: " . $e->getMessage();
exit;
}
?>

View File

@@ -119,6 +119,13 @@
flex-direction: column;
background-color: var(--bg-editor);
border-right: 2px solid #333;
transition: width 0.3s ease, transform 0.3s ease;
}
#editor-container.collapsed {
width: 0;
transform: translateX(-100%);
border-right: none;
}
#editor {
@@ -136,6 +143,11 @@
align-items: center;
justify-content: center;
overflow: hidden;
transition: width 0.3s ease;
}
#preview-container.full-width {
width: 100%;
}
canvas {
@@ -165,6 +177,110 @@
width: 100%;
height: 100%;
}
/* Share Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal-content {
background-color: #2c292d;
border-radius: 8px;
padding: 24px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.modal-header {
font-size: 20px;
font-weight: 600;
margin-bottom: 16px;
color: #f0f0f0;
}
.modal-body {
margin-bottom: 20px;
}
.link-container {
display: flex;
gap: 8px;
margin-top: 12px;
}
.link-input {
flex: 1;
padding: 10px 12px;
background-color: #1e1e1e;
border: 1px solid #444;
border-radius: 4px;
color: #f0f0f0;
font-family: 'Consolas', monospace;
font-size: 14px;
}
.copy-btn {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s;
}
.copy-btn:hover {
background-color: #45a049;
}
.copy-btn.copied {
background-color: #2196F3;
}
.modal-footer {
display: flex;
justify-content: flex-end;
}
.close-btn {
padding: 10px 20px;
background-color: #555;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s;
}
.close-btn:hover {
background-color: #666;
}
.loading-text {
color: #aaa;
font-style: italic;
}
.error-text {
color: #ff6b6b;
margin-top: 8px;
}
</style>
<header>
@@ -172,7 +288,7 @@
<button class="label-btn" title="PyStudio">
<p>PyStudio</p>
</button>
<button class="icon-btn" title="Run Code" onclick="runCurrentCode()">
<button id="runButton" class="icon-btn" title="Run Code" onclick="runCurrentCode()">
<svg viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
@@ -182,6 +298,30 @@
<path d="M6 6h12v12H6z" />
</svg>
</button>
<button class="icon-btn" id="toggleEditorBtn" title="Toggle Code Editor" onclick="toggleEditorLayout()">
<svg viewBox="0 0 24 24">
<path d="M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h4v8H8V8z" />
</svg>
</button>
</div>
<div class="header-right">
<button class="icon-btn" title="Share Code" onclick="shareCode()">
<svg viewBox="0 0 24 24">
<path
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z" />
</svg>
</button>
<button class="icon-btn" title="Open File" onclick="openFile()">
<svg viewBox="0 0 24 24">
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
</svg>
</button>
<button class="icon-btn" title="Save File (Ctrl+S)" onclick="saveFile()">
<svg viewBox="0 0 24 24">
<path
d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z" />
</svg>
</button>
</div>
</header>
@@ -197,6 +337,31 @@
</div>
</main>
<!-- Share Modal -->
<div id="shareModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">Share Your Code</div>
<div class="modal-body">
<div id="modalMessage">
<p class="loading-text">Uploading code...</p>
</div>
<div id="linkSection" style="display: none;">
<p>Your code has been shared! Use this link:</p>
<div class="link-container">
<input type="text" id="shareLink" class="link-input" readonly>
<button class="copy-btn" onclick="copyShareLink()">Copy</button>
</div>
</div>
<div id="errorSection" style="display: none;">
<p class="error-text" id="errorMessage"></p>
</div>
</div>
<div class="modal-footer">
<button class="close-btn" onclick="closeShareModal()">Close</button>
</div>
</div>
</div>
<script src="https://www.unpkg.com/ace-builds@latest/src-noconflict/ace.js" crossorigin="anonymous"></script>
<script>
@@ -287,6 +452,10 @@
function runCurrentCode() {
stopCurrentCode();
document.getElementById('runButton').blur();
document.querySelector('canvas').focus();
if (term) term.clear();
StopExecution = false;
var code = codeEditor.getValue();
@@ -317,6 +486,29 @@
});
}
function toggleEditorLayout() {
const editorContainer = document.getElementById('editor-container');
const previewContainer = document.getElementById('preview-container');
const toggleBtn = document.getElementById('toggleEditorBtn');
editorContainer.classList.toggle('collapsed');
previewContainer.classList.toggle('full-width');
// Toggle icon/title
if (editorContainer.classList.contains('collapsed')) {
toggleBtn.title = "Show Code Editor";
toggleBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4V4zm2 2v12h12V6H6zm10 2h-4v8h4V8z" /></svg>`;
} else {
toggleBtn.title = "Hide Code Editor";
toggleBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4V4zm2 2v12h12V6H6zm2 2h4v8H8V8z" /></svg>`;
}
// Trigger resize after transition
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 300);
}
function toggleEditor() {
runCurrentCode();
}
@@ -331,15 +523,229 @@
}
}
// File System Access API support detection
let currentFileHandle = null;
async function saveFile() {
try {
// Check if File System Access API is supported
if ('showSaveFilePicker' in window) {
// Use existing file handle if available, otherwise prompt for new file
if (!currentFileHandle) {
const options = {
types: [{
description: 'Python Files',
accept: { 'text/x-python': ['.py'] },
}],
suggestedName: 'sketch.py'
};
currentFileHandle = await window.showSaveFilePicker(options);
}
const writable = await currentFileHandle.createWritable();
await writable.write(codeEditor.getValue());
await writable.close();
term.write('\r\n✓ File saved successfully\r\n');
} else {
// Fallback to download for browsers that don't support File System Access API
downloadCode();
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Save failed:', err);
term.write('\r\n✗ Save failed: ' + err.message + '\r\n');
}
}
}
async function openFile() {
try {
if ('showOpenFilePicker' in window) {
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: 'Python Files',
accept: { 'text/x-python': ['.py'] },
}],
multiple: false
});
currentFileHandle = fileHandle;
const file = await fileHandle.getFile();
const contents = await file.text();
codeEditor.setValue(contents, -1);
term.write('\r\n✓ File opened: ' + file.name + '\r\n');
} else {
// Fallback to file input for browsers that don't support File System Access API
const input = document.createElement('input');
input.type = 'file';
input.accept = '.py';
input.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
const contents = await file.text();
codeEditor.setValue(contents, -1);
term.write('\r\n✓ File opened: ' + file.name + '\r\n');
}
};
input.click();
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Open failed:', err);
term.write('\r\n✗ Open failed: ' + err.message + '\r\n');
}
}
}
function downloadCode() {
const code = codeEditor.getValue();
const blob = new Blob([code], { type: 'text/x-python' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'sketch.py';
link.click();
URL.revokeObjectURL(url);
term.write('\r\n✓ File downloaded\r\n');
}
document.addEventListener('keydown', function (e) {
// Ctrl+Enter or Cmd+Enter to run code
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
runCurrentCode();
}
// Ctrl+S or Cmd+S to save file
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveFile();
}
// Ctrl+O or Cmd+O to open file
if ((e.ctrlKey || e.metaKey) && e.key === 'o') {
e.preventDefault();
openFile();
}
});
async function shareCode() {
const modal = document.getElementById('shareModal');
const modalMessage = document.getElementById('modalMessage');
const linkSection = document.getElementById('linkSection');
const errorSection = document.getElementById('errorSection');
const errorMessage = document.getElementById('errorMessage');
modalMessage.style.display = 'block';
modalMessage.innerHTML = '<p class="loading-text">Uploading code...</p>';
linkSection.style.display = 'none';
errorSection.style.display = 'none';
modal.classList.add('active');
try {
const code = codeEditor.getValue();
const response = await fetch('/codestorage/default.php', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: code
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.status === 'success' && data.id) {
const url = new URL(window.location.href);
url.searchParams.set('id', data.id);
const shareableLink = url.toString();
modalMessage.style.display = 'none';
linkSection.style.display = 'block';
document.getElementById('shareLink').value = shareableLink;
term.write('\r\n✓ Code shared successfully! ID: ' + data.id + '\r\n');
} else {
throw new Error('Invalid response from server');
}
} catch (error) {
console.error('Share failed:', error);
modalMessage.style.display = 'none';
errorSection.style.display = 'block';
errorMessage.textContent = 'Failed to share code: ' + error.message;
term.write('\r\n✗ Share failed: ' + error.message + '\r\n');
}
}
function copyShareLink() {
const linkInput = document.getElementById('shareLink');
const copyBtn = event.target;
linkInput.select();
linkInput.setSelectionRange(0, 99999);
navigator.clipboard.writeText(linkInput.value).then(() => {
const originalText = copyBtn.textContent;
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.classList.remove('copied');
}, 2000);
term.write('\r\n✓ Link copied to clipboard\r\n');
}).catch(err => {
console.error('Copy failed:', err);
term.write('\r\n✗ Failed to copy link\r\n');
});
}
function closeShareModal() {
const modal = document.getElementById('shareModal');
modal.classList.remove('active');
}
document.getElementById('shareModal').addEventListener('click', function (e) {
if (e.target === this) {
closeShareModal();
}
});
async function loadSharedCode() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
if (id) {
try {
const response = await fetch(`/codestorage/default.php?id=${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const code = await response.text();
codeEditor.setValue(code, -1);
term.write('\r\n✓ Loaded shared code (ID: ' + id + ')\r\n');
} catch (error) {
console.error('Failed to load shared code:', error);
term.write('\r\n✗ Failed to load shared code: ' + error.message + '\r\n');
}
}
}
window.addEventListener('load', () => {
setTimeout(runCurrentCode, 100);
loadSharedCode().then(() => {
setTimeout(runCurrentCode, 100);
});
});
window.addEventListener('resize', () => {

View File

@@ -250,6 +250,18 @@ var $builtinmodule = function (name) {
mod.sqrt = new Sk.builtin.func((n) => toPy(Math.sqrt(toJS(n))));
mod.pause = new Sk.builtin.func(() => { pause(); return Sk.builtin.none.none$; });
mod.resume = new Sk.builtin.func(() => { resume(); return Sk.builtin.none.none$; });
mod.pal = new Sk.builtin.func((colors, textColor) => {
pal(colors !== undefined ? toJS(colors) : undefined, textColor !== undefined ? toJS(textColor) : 3);
return Sk.builtin.none.none$;
});
mod.palc = new Sk.builtin.func((a, b) => {
palc(a !== undefined ? toJS(a) : undefined, b !== undefined ? toJS(b) : undefined);
return Sk.builtin.none.none$;
});
mod.paint = new Sk.builtin.func((width, height, data, options) => {
var result = paint(toJS(width), toJS(height), toJS(data), options !== undefined ? toJS(options) : undefined);
return result ? toPy(result) : Sk.builtin.none.none$;
});
return mod;
};