Skip to content

Instantly share code, notes, and snippets.

@boreals-back-again
Created August 16, 2025 05:03
Show Gist options
  • Save boreals-back-again/64a34a3d8578ac11c2c23aedaa948366 to your computer and use it in GitHub Desktop.
Save boreals-back-again/64a34a3d8578ac11c2c23aedaa948366 to your computer and use it in GitHub Desktop.
GN File Tools
// ==UserScript==
// @name GN File Tools
// @namespace http://tampermonkey.net/
// @version 1.0
// @description File editor for GN Math.
// @match https://gn-math.github.io/*
// @grant none
// ==/UserScript==
function readFileAsArrayBuffer(file) {
console.log("[readFileAsArrayBuffer] Starting to read file:", file);
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => {
console.log("[readFileAsArrayBuffer] File read successfully.");
resolve(e.target.result);
};
reader.onerror = e => {
console.error("[readFileAsArrayBuffer] Error reading file:", e.target.error);
reject(new Error("Error reading file: " + e.target.error));
};
reader.readAsArrayBuffer(file);
console.log("[readFileAsArrayBuffer] FileReader started.");
});
}
function openFileEditor() {
console.log("[openFileEditor] Opening file tools popup.");
document.getElementById('popupTitle').textContent = "File Tools";
const popupBody = document.getElementById('popupBody');
popupBody.innerHTML = `
<h2>How to upload DELTARUNE save</h2>
<p>Enter "/_savedata" in the IndexedDB name and then upload each file from your save, one by one.</p>
<h3>Extract files</h3>
<button id="extractFilesButton">Extract as ZIP</button>
<h3>Add files</h3>
<label for="dbNameInput">IndexedDB Name:</label><br/>
<input type="text" id="dbNameInput" placeholder="Enter IndexedDB name" /><br/><br/>
<label for="fileUploadButton">Upload a file to save:</label><br/>
<input type="file" id="fileUploadButton" /><br/><br/>
<button id="saveFileButton">Save File</button>
`;
document.getElementById('popupOverlay').style.display = "flex";
document.getElementById('saveFileButton').onclick = saveFile;
document.getElementById('extractFilesButton').onclick = extractFilesAsZip;
};
async function extractFilesAsZip() {
if (typeof JSZip === "undefined") {
await new Promise((resolve) => {
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js";
script.onload = resolve;
document.head.appendChild(script);
});
}
const zip = new JSZip();
const dbs = await indexedDB.databases();
for(const dbInfo in dbs) {
const dbName = dbs[dbInfo].name;
console.log(dbName);
if (!dbName) continue;
const request = indexedDB.open(dbName);
const db = await new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const objectStoreNames = Array.from(db.objectStoreNames);
// get "FILE_DATA" object store
if (!objectStoreNames.includes("FILE_DATA")) {
continue;
}
const transaction = db.transaction("FILE_DATA", "readonly");
const objectStore = transaction.objectStore("FILE_DATA");
await new Promise((resolve, reject) => {
const request = objectStore.openCursor();
request.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
console.log("Key:", cursor.key);
console.log("Value object:", cursor.value);
if(cursor.value.contents) {
zip.file(cursor.key, cursor.value.contents.buffer, cursor.value.timestamp);
}
cursor.continue();
} else {
resolve(); // finished iterating
}
};
request.onerror = event => reject(event.target.error);
});
db.close();
}
zip.generateAsync({ type: "blob" }).then(content => {
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = new Date().toISOString() + ".zip";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
async function saveFile() {
console.log("[saveFile] Starting save file process.");
const dbNameInput = document.getElementById('dbNameInput');
const fileInput = document.getElementById('fileUploadButton');
const dbName = dbNameInput.value.trim();
const files = fileInput.files;
if (!dbName) {
alert("Please enter an IndexedDB database name.");
return;
}
if (!files.length) {
alert("Please select at least one file.");
return;
}
// Open DB before loop to avoid reopening for each file
const openRequest = indexedDB.open(dbName, 21);
openRequest.onupgradeneeded = event => {
const db = event.target.result;
if (!db.objectStoreNames.contains('FILE_DATA')) {
console.log("[IndexedDB] Creating object store 'FILE_DATA'.");
const store = db.createObjectStore('FILE_DATA', { keyPath: 'path' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
openRequest.onerror = () => {
alert("Failed to open database: " + openRequest.error);
};
openRequest.onsuccess = async event => {
const db = event.target.result;
try {
for (const file of files) {
const fileArray = await readFileAsArrayBuffer(file);
const fileName = dbName + "/" + file.name;
// Open a new transaction for each file to keep it active only while putting the file
await new Promise((resolve, reject) => {
const transaction = db.transaction('FILE_DATA', 'readwrite');
const store = transaction.objectStore('FILE_DATA');
const putRequest = store.put({
timestamp: new Date(),
mode: 33206,
contents: new Int8Array(fileArray)
}, fileName);
putRequest.onsuccess = () => {
};
putRequest.onerror = () => {
console.error("[saveFile] Error saving file:", putRequest.error);
reject(putRequest.error);
};
transaction.oncomplete = () => {
resolve();
};
transaction.onerror = () => {
reject(transaction.error);
};
});
}
alert("All files saved successfully.");
} catch (e) {
alert("Error during saving files: " + e.message);
}
};
}
(function() {
'use strict';
const footer = document.querySelector('.footer-links');
if (footer) {
const link = document.createElement('a');
link.href = '#';
link.textContent = 'File Tools';
link.onclick = function() {
openFileEditor();
return false;
};
footer.appendChild(link);
console.log("[UserScript] File Tools link added to footer.");
} else {
console.warn("[UserScript] Footer element '.footer-links' not found.");
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment