// ==UserScript== // @name Evolve Idle Cloud Save // @namespace https://github.com/Alistair1231/my-userscripts/ // @version 1.3.3 // @description Automatically upload your evolve save to a gist // @author Alistair1231 // @match https://pmotschmann.github.io/Evolve/ // @icon https://icons.duckduckgo.com/ip2/github.io.ico // @license GPL-3.0 // @grant GM.addStyle // @grant GM.xmlHttpRequest // @require https://cdn.jsdelivr.net/npm/@trim21/gm-fetch@0.2.1 // @downloadURL https://update.greasyfork.icu/scripts/490376/Evolve%20Idle%20Cloud%20Save.user.js // @updateURL https://update.greasyfork.icu/scripts/490376/Evolve%20Idle%20Cloud%20Save.meta.js // ==/UserScript== // https://greasyfork.org/en/scripts/490376-automatic-evolve-save-upload-to-gist // https://github.com/Alistair1231/my-userscripts/raw/master/EvolveIdleSavegameBackup.user.js /* # Evolve Idle Cloud Save I lost my save game 😞, so I created a quick backup solution using GitHub Gist to store save data. ### Key Features: - **Automatic Upload:** On first use, you'll be prompted to enter your Gist ID and Personal Access Token. These credentials are stored as plain text in the Userscript storage. The token must have the `gist` scope. - **Manual Setup:** You need to manually create the Gist and enter its ID in the settings. - **Export Settings:** Saves are exported to the filename specified in the settings. - **Import Flexibility:** Import your save from any file in the Gist, making it easy to restore data after switching devices or PCs. - **Backup Options:** - Automatic backups are performed every 10 minutes. - Manual backups can be triggered by clicking the "Save to" button. - **Advanced Use:** The `evolveCloudSave` object is exposed to the window, allowing for manual interaction. With this setup, your progress is secure, and you can easily transfer your saves between devices. ![UI changes](https://i.imgur.com/G1QCIXU.png) */ ;(async function () { 'use strict' /** * Settings utility object for managing localStorage data */ const storage = { /** * Retrieves and parses a JSON value from localStorage * @async * @param {string} key - Storage key to retrieve * @returns {Promise} Parsed JSON value */ get: async (key) => { key = `evolveCloudSave_${key}` const value = localStorage[key] return value === undefined ? null : JSON.parse(value) }, /** * Stringifies and stores a value in localStorage * @async * @param {string} key - Storage key to set * @param {any} value - Value to stringify and store * @returns {Promise} Stringified value that was stored */ set: async (key, value) => { key = `evolveCloudSave_${key}` return (localStorage[key] = JSON.stringify(value)) }, /** * Lists all storage keys after splitting on underscore * @async * @returns {Promise} Array of storage key second parts */ list: async () => { let keys = Object.keys(localStorage) // filter out keys that don't start with "evolveCloudSave_" keys = keys.filter((key) => key.startsWith('evolveCloudSave_')) // remove the "evolveCloudSave_" prefix keys = keys.map((key) => key.replace('evolveCloudSave_', '')) return keys }, /** * Removes an item from localStorage * @async * @param {string} key - Storage key to delete * @returns {Promise} */ delete: async (key) => { key = `evolveCloudSave_${key}` delete localStorage[key] }, } /** * Waits for an element matching the selector to appear in the DOM * @param {string} selector - CSS selector to match element * @param {function} callback - Function to execute when element is found * @param {number} [interval=100] - Time in ms between checks for element * @param {number} [timeout=5000] - Maximum time in ms to wait before giving up */ function waitFor(selector, callback, interval = 100, timeout = 5000) { const startTime = Date.now() const check = () => { const element = document.querySelector(selector) if (element) { callback(element) } else if (Date.now() - startTime < timeout) { setTimeout(check, interval) } } check() } const evolveCloudSave = { // Create an overlay to collect secrets from the user openSettings: () => { const saveSettings = () => { const gistId = document.getElementById('gist_id').value.trim() const token = document.getElementById('gist_token').value.trim() const frequency = document.getElementById('save_frequency').value.trim() || '10' const filename = document.getElementById('file_name').value.trim() || 'save.txt' if (!gistId || !token) { alert('Gist ID and Token are required!') return } storage.set('gistId', gistId) storage.set('token', token) storage.set('filename', filename) storage.set('frequency', frequency) document.body.removeChild(overlay) } const fillCurrentSettings = async () => { const gistId = await storage.get('gistId') const token = await storage.get('token') const filename = await storage.get('filename') const frequency = await storage.get('frequency') document.getElementById('gist_id').value = gistId || '' document.getElementById('gist_token').value = token || '' document.getElementById('file_name').value = filename || 'save.txt' document.getElementById('save_frequency').value = frequency || '10' } let overlay = document.createElement('div') overlay.innerHTML = `
You will need a GistID (last part of URL when viewing a Gist) and a Personal Access Token to use this cloud-save script. Create a gist here, and a token here
` document.body.appendChild(overlay) // clicking on overlay and esc handling overlay.addEventListener('click', (e) => { if (e.target.id === 'settings_overlay') { document.body.removeChild(overlay) } }) document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (document.getElementById('settings_overlay')) { document.body.removeChild(overlay) } } }) document.getElementById('save_button').addEventListener('click', (e) => { e.preventDefault() saveSettings() // force refresh location.reload() }) fillCurrentSettings() }, getFiles: async () => { const gistId = await storage.get('gistId') const token = await storage.get('token') let files = await GM_fetch(`https://api.github.com/gists/${gistId}`, { method: 'GET', headers: { Authorization: `token ${token}` }, }) if (files.status === 200) { files = await files.json() return files.files } else { console.log(files) return {} } }, createOrUpdateFile: async (filename, content) => { const files = await evolveCloudSave.getFiles() const gistId = await storage.get('gistId') const token = await storage.get('token') if (files[filename] === undefined) { let response = await GM_fetch( `https://api.github.com/gists/${gistId}`, { method: 'POST', headers: { Authorization: `token ${token}` }, body: `{ "files": { "${filename}": { "content": "${content}" } } }`, } ) return response } else { let response = await GM_fetch( `https://api.github.com/gists/${gistId}`, { method: 'PATCH', headers: { Authorization: `token ${token}` }, body: `{ "files": { "${filename}": { "content": "${content}" } } }`, } ) return response } }, makeBackup: async () => { const saveString = unsafeWindow.exportGame() const filename = await storage.get('filename') const response = await evolveCloudSave.createOrUpdateFile( filename, saveString ) return response }, getBackup: async () => { const remote_files = await evolveCloudSave.getFiles() const remote_filename = document.getElementById( 'cloudsave_fileSelect' ).value const content = remote_files[remote_filename].content document.querySelector('textarea#importExport').value = content }, addButtons: async () => { const buttons = document.createElement('div') const remote_files = await evolveCloudSave.getFiles() const remote_filenames = Object.keys(remote_files) const local_filename = await storage.get('filename') buttons.innerHTML = `

` const div = document.querySelectorAll('div.importExport')[1] div.appendChild(buttons) document .getElementById('cloudsave_importGistButton') .addEventListener('click', () => { evolveCloudSave.getBackup() }) document .getElementById('cloudsave_exportGistButton') .addEventListener('click', async () => { const response = await evolveCloudSave.makeBackup() if (response.status === 200) { const successMessage = document.getElementById('success_message') successMessage.style.display = 'block' setTimeout(() => { successMessage.style.transition = 'opacity 1s' successMessage.style.opacity = '0' setTimeout(() => { successMessage.style.display = 'none' successMessage.style.opacity = '1' }, 1000) }, 2000) } console.log(response) }) document .getElementById('cloudsave_settingsButton') .addEventListener('click', () => { evolveCloudSave.openSettings() }) }, } waitFor('div#main', async () => { GM.addStyle(` .material-input { position: relative; margin-top: 15px; font-size: 14px; } .material-input input { width: 100%; padding: 10px 5px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; outline: none; } .material-input input:focus { border-color: #6200ee; } .material-input label { position: absolute; top: 50%; left: 10px; transform: translateY(-50%); transition: all 0.2s ease-out; color: #999; font-size: 14px; pointer-events: none; background: white; padding: 0 4px; } .material-input input:focus + label, .material-input input:not(:placeholder-shown) + label { top: -8px; transform: translateY(0); font-size: 12px; color: #6200ee; }`) const gistId = await storage.get('gistId') const token = await storage.get('token') const frequency = await storage.get('frequency') if (gistId === null || token === null) { evolveCloudSave.openSettings() return } else { evolveCloudSave.addButtons() // run every 10 minutes setInterval(evolveCloudSave.makeBackup, 1000 * 60 * frequency) // export for manual use unsafeWindow.evolveCloudSave = evolveCloudSave unsafeWindow.evolveCloudSave.settings = storage } }) })()