Created
          August 16, 2025 05:03 
        
      - 
      
- 
        Save boreals-back-again/64a34a3d8578ac11c2c23aedaa948366 to your computer and use it in GitHub Desktop. 
    GN File Tools
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | // ==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