// ==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
// ==/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<any>} 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<string>} 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<string[]>} 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<void>}
*/
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 = `
<div id="settings_overlay"
style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 1000">
<div id="settings_modal"
style="background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); width: 400px">
<div style="color: #333; font-size: 14px; margin-bottom: 15px; line-height: 1.4">
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 <a href="https://gist.github.com/">here</a>, and a token <a href="https://github.com/settings/tokens/new?scopes=gist&description=EvolveIdleSavegameBackup">here</a>
</div>
<form id="settings_form">
<div class="material-input" style="margin-bottom: 15px">
<input id="gist_id" type="text" required style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px">
<label for="gist_id" style="color: #666; font-size: 12px; margin-top: 4px; display: block">Gist ID</label>
</div>
<div class="material-input" style="margin-bottom: 15px">
<input id="gist_token" type="text" required style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px">
<label for="gist_token" style="color: #666; font-size: 12px; margin-top: 4px; display: block">Token with Gist scope</label>
</div>
<div class="material-input" style="margin-bottom: 15px">
<input id="file_name" type="text" required style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px">
<label for="file_name" style="color: #666; font-size: 12px; margin-top: 4px; display: block">Filename</label>
</div>
<div class="material-input" style="margin-bottom: 15px">
<input id="save_frequency" type="text" required style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px">
<label for="save_frequency" style="color: #666; font-size: 12px; margin-top: 4px; display: block">Save Frequency in minutes</label>
</div>
<button id="save_button" style="width: 100%; padding: 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background-color 0.3s">Save</button>
</form>
</div>
</div>
`
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 = `
<div class='importExport' style='display: flex; margin-top: 1rem'>
<button id='cloudsave_importGistButton' class='button' style='margin-top: .75rem;marging-right=1em'>Import selected</button>
<select id='cloudsave_fileSelect' style='margin-top: .75rem'>
${remote_filenames.map((file) => `<option value='${file}'>${file}</option>`)}
</select>
</div>
<button id='cloudsave_exportGistButton' class='button' style='margin-top: .75rem'>Save to "${local_filename}"</button>
<br>
<button id='cloudsave_settingsButton' class='button' style='margin-top: .75rem'>Settings</button>
<div id='success_message' style='display: none; position: fixed; top: 20px; right: 20px; background-color: green; color: white; padding: 10px; border-radius: 5px;'>Backup successful!</div>
`
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
}
})
})()