// ==UserScript== // @name Tumblr Post Saver // @namespace https://github.com/bab5470/tampermonkey-tumblr-post-saver // @version 0.2 // @description Save tumblr reblogs and drafts on a timer or manually. Prompts you to save new posts to draft (so they aren't lost). Restore posts automatically (after a browser crash). Options to import, export, or clear saved post data. // @author Brad Baker // // @match *://www.tumblr.com/* // // @noframes // // @grant GM_addStyle // @grant GM_listValues // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant unsafeWindow // @grant GM_registerMenuCommand // // @run-at document-end // // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @require https://greasyfork.org/scripts/2199-waitforkeyelements/code/waitForKeyElements.js?version=6349 // @require http://code.jquery.com/jquery-latest.js // // @downloadURL https://update.greasyfork.icu/scripts/35775/Tumblr%20Post%20Saver.user.js // @updateURL https://update.greasyfork.icu/scripts/35775/Tumblr%20Post%20Saver.meta.js // ==/UserScript== GM_config.init({ 'id': 'tumblr_post_saver', // The id used for this instance of GM_config 'title': 'Tumblr Post Saver Settings', 'fields': // Fields object { 'time_between_saves': // This is the id of the field { 'label': 'Time Between Saves', // Appears next to field 'section': ['Settings', 'Set options below.'], // Appears above the field 'type': 'select', // Makes this setting a text field 'options': ['5000', '30000', '60000'], // Possible choices 'default': '5000' // Default value if user doesn't change it }, 'days_to_keep': // This is the id of the field { 'label': 'Days to Keep', // Appears next to field 'type': 'select', // Makes this setting a text field 'options': ['183', '365', '730'], // Possible choices 'default': '730' // Default value if user doesn't change it }, 'backup_posts': { 'label': 'Backup Posts', // Appears on the button 'section': ['Post Management', 'Backup, Restore, Clear Posts'], // Appears above the field 'type': 'button', // Makes this setting a button input 'size': 100, // Control the size of the button (default is 25) 'click': function() { // Function to call when button is clicked download_data(); } }, 'restore_posts': { 'label': 'Restore Posts', // Appears on the button 'type': 'button', // Makes this setting a button input 'size': 100, // Control the size of the button (default is 25) 'click': function() { // Function to call when button is clicked upload_data(); } }, 'clear_data': { 'label': 'Clear Posts', // Appears on the button 'type': 'button', // Makes this setting a button input 'size': 100, // Control the size of the button (default is 25) 'click': function() { // Function to call when button is clicked clear_data(); } }, }, 'css': '#tumblr_post_saver_restore_posts_var #tumblr_post_saver_clear_data_var { width = 10px; float:left; }' // CSS that will hide the section }); var fireOnHashChangesToo = true; var pageURLCheckTimer = setInterval ( function () { if (this.lastPathStr !== location.pathname || this.lastQueryStr !== location.search || (fireOnHashChangesToo && this.lastHashStr !== location.hash)) { this.lastPathStr = location.pathname; this.lastQueryStr = location.search; this.lastHashStr = location.hash; Main (); } }, 1000 ); function Main () { 'use strict'; console.log ('A new page has loaded.'); // Add CSS GM_addStyle('#post_saver_button { font-size: 13px;font-weight: 700; padding-top: 5px;padding-bottom: 5px;border-radius: 2px 2px 2px 2px;padding-left: 10px;padding-right: 10px;border-color: #4a9aca;background-color: #4a9aca;color: hsla(0,0%,100%,.9);float:left;}'); GM_addStyle('#white_div {font-size: 13px;font-weight: 700;padding-top: 5px;padding-bottom: 5px;padding-left: 10px;padding-right: 10px;background-color: #ffffff;float:left;}'); // Register a menu entry for configuring post saver GM_registerMenuCommand('Tumblr Post Saver: Configuration', function() { GM_config.open(); }); if ((GM_config.get('time_between_saves'))) { console.log('Time between saves: ' + GM_config.get('time_between_saves')); } if ((GM_config.get('days_to_keep'))) { console.log('Days to keep: ' + GM_config.get('days_to_keep')); } // Get the current date stamp and the datestamp that the purge was last run. var current_datestamp = build_current_datestamp(); var last_time_purge_ran_datestamp = GM_getValue("last_ran_purge_datestamp"); // If the last ran purage datestamp isn't set this must be the first run. // Set it. if (last_time_purge_ran_datestamp === null) { // Write the latest run to the database GM_setValue("last_ran_purge_datestamp",current_datestamp); } // If a purge hasn't been run in the last date run it. if (last_time_purge_ran_datestamp < current_datestamp) { prune_old_posts(); } if(location.pathname.match(/reblog/)) { console.log("reblog"); reblog(); } else if (location.pathname.match(/edit/)) { console.log("edit"); edit(); } else if (location.pathname.match(/drafts/)) { console.log("drafts"); // Do nothing } else { console.log("couldn't find reblog, edit or drafts so polling visibility"); pollVisibility(); } } function pollVisibility() { // The post form is open (it could be a reblog or a new post. We'll check that below. if( ($('.post-forms-glass').is(':visible')) && (($('.reblog_name').length) === 0)) { console.log("new post"); newpost(); } else { console.log("poll visibility again"); if(location.pathname.match(/reblog/)) { console.log("reblog"); reblog(); } else if (location.pathname.match(/edit/)) { console.log("edit"); edit(); } else if (location.pathname.match(/drafts/)) { console.log("drafts"); // Do nothing } setTimeout(pollVisibility, 5000); } } function newpost () { // Every "time_between_saves" (see preferences) //setInterval(function() { if (!location.pathname.match(/drafts/)) { // Keep prompting the user to save the post as otherwise it may be lost during a browser crash/OS reboot etc. if (confirm("Warning: It appears you're starting a new post. Post saver can't save new posts until you save an initial draft. Would you like to do so now? (You won't be prompted again)")) { // Click the arrow drop down next to the post button document.getElementsByClassName('dropdown-area icon_arrow_carrot_down')[0].click(); // Find the save as draft list item and click it var save_as_draft = document.evaluate("//span[contains(., 'Save as draft')]", document, null, XPathResult.ANY_TYPE, null ); var save_as_draft_link = save_as_draft.iterateNext(); save_as_draft_link.click(); // Click the save button var save_button = $('.post-form--save-button [data-js-clickablesave]'); save_button.click(); } } //}, GM_config.get('time_between_saves')); } function reblog () { // Get the request ID from the URL request = get_request_id(); console.log(request); // Get all the saved posts var saved_posts = GM_listValues(); // Generate a regular expression to match the request id var regex = new RegExp(request, "g"); // Create variable to store the key var datestamp; // Iterate through the posts for (var i=0; i < saved_posts.length; i++) { var key = saved_posts[i]; // If there's a match then set the post content to the key data if (key.match(regex)) { console.log("Match found for " + regex); // Store the current key in a datestamp variable for use below datestamp = key; // Get the matching post from the database var postcontent = GM_getValue(saved_posts[i]); // Restore the post if (document.getElementsByClassName('editor editor-richtext')[0]) { document.getElementsByClassName('editor editor-richtext')[0].innerHTML = postcontent; } else { setTimeout(reblog, 1000); } } else { console.log("Match NOT found for " + regex); } } // Insert save button insert_save_button(datestamp); // Every "time_between_saves", save the post to the database setInterval(function() { var key; // If the datestamp is null (i.e. we've never seen this post before) then create a new key // otherwise reuse the existing key if(datestamp) { key = datestamp; } else { key = build_current_datestamp() + "_" + get_request_id(); } var postcontent = document.getElementsByClassName('editor editor-richtext')[0]; // If the post content is


then the post field is empty. Don't save as // there's no point in saving an empty post and also there can be a race condition // where the post hasn't been restored and thus is empty and the automatic save kicks in. // If that happens the saved post can be wipped out with


if(postcontent.innerHTML === "


"){ console.log("Post not saved as post content is empty: " + postcontent.innerHTML); } else if (postcontent.innerHTML === GM_getValue(key)) { // Don't do anything as the post is already up to date. // console.log("Post already up to date: " + key); } else { // console.log("Post saved as: " + key); //console.log("Post Content: " + postcontent.innerHTML); // Save the post GM_setValue(key,postcontent.innerHTML); // Check that the post actually saved! if (GM_getValue(key) !== postcontent.innerHTML) { alert("Post not saved! Try again"); } } }, GM_config.get('time_between_saves')); } function edit () { // alert("edit"); // Create variable to store the key var datestamp; // Get the request ID from the URL var request = get_request_id(); // Get all the saved posts var saved_posts = GM_listValues(); // Generate a regular expression to match the request id var regex = new RegExp(request, "g"); // Create variable to store the key var matched_key; // Iterate through the posts for (var i=0; i < saved_posts.length; i++) { var current_key = saved_posts[i]; // If the db entry's key matches the request id we're looking for if (current_key.match(regex)) { // Store the match in the matched_key variable matched_key = current_key; // Get the post content from the database var saved_post_content = GM_getValue(saved_posts[i]); // Get the post content stored by tumblr var current_post_content = document.getElementsByClassName('editor editor-richtext')[0].innerHTML; // If the two don't match - houston we have a problem // if (saved_post_content != current_post_content){ // Ask the user what we want to do. Is tumblr right or is post_saver right? // if (confirm('I found a draft different than the one here. Do you want to restore it?')) { document.getElementsByClassName('editor editor-richtext')[0].innerHTML = saved_post_content; // } } } var key; // If the matched_key is null (i.e. we've never seen this post before) then create a new key // otherwise reuse the existing key if(matched_key === null) { key = build_current_datestamp() + "_" + get_request_id(); } else { key = matched_key; } // Save the post to the database in case its not already there var postcontent = document.getElementsByClassName('editor editor-richtext')[0]; if(postcontent){ // Save the post GM_setValue(key,postcontent.innerHTML); // Check that the post actually saved! if (GM_getValue(key) !== postcontent.innerHTML) { alert("Post not saved! Try again"); } } // Insert save button insert_save_button(datestamp); // Save the post again every time_between_saves setInterval(function() { var postcontent = document.getElementsByClassName('editor editor-richtext')[0]; if(postcontent){ // First check if the post is already saved (there's no sense in repeatedly saving something thats already in the database) if (GM_getValue(key) !== postcontent.innerHTML) { // Save the post GM_setValue(key,postcontent.innerHTML); // Check again that it actually saved. If it didn't alert the user if (GM_getValue(key) !== postcontent.innerHTML) { alert("Auto save failed. You may want to try manual saving."); } } } }, GM_config.get('time_between_saves')); } // Get the tumbler post id (request id) from the URL function get_request_id (){ // Get the current url path var location_path = location.pathname; // Split on the slash character var location_items = location_path.split("/"); // Skip first path (reblog/edit, etc) in the url location_items.shift(); // Get the ID from the URL and return it var request = parseInt(location_items[1]); return request; } function insert_save_button (datestamp) { // Create a new button var save_draft_button = document.createElement('button'); save_draft_button.setAttribute("id", "post_saver_button"); save_draft_button.innerHTML = 'Save to Post Saver'; // Set what happens when the button is clicked save_draft_button.onclick = function(){ // Instantiate a variable to store the key var key; // If the datestamp is null (i.e. we've never seen this post before) then create a new key // otherwise reuse the existing key if(datestamp === null) { key = build_current_datestamp() + "_" + get_request_id(); } else { key = datestamp; } var postcontent = document.getElementsByClassName('editor editor-richtext')[0]; GM_setValue(key,postcontent.innerHTML); // Check that the post actually saved! if (GM_getValue(key) !== postcontent.innerHTML) { alert("Post not saved! Try again"); } else { alert("saved!"); } return false; }; var checkExist = setInterval(function() { // Find the create post button var save_button = document.getElementsByClassName('button-area create_post_button')[0]; if (save_button) { console.log("Post button exists."); // Check of the post saver button already exits so we don't add it multiple times. if (document.getElementById('post_saver_button') === null || document.getElementById('post_saver_button') === undefined) { console.log("Post saver button not found to adding it"); // Insert the save_draft button before the save button save_button.parentNode.insertBefore(save_draft_button, save_button); // Create a white div to create space between the post saver and post button var white_div = document.createElement('div'); white_div.innerHTML = '  '; white_div.setAttribute("id", "white_div"); // Insert the white space before the save button save_button.parentNode.insertBefore(white_div, save_button); } clearInterval(checkExist); } else { console.log("Post button does not exist"); } }, 100); // check every 100ms } function clear_data (){ if (confirm('Are you sure you to clear all data? This cannot be undone!!!')) { // Clear everything fom the database var keys = GM_listValues(); for (var key of keys) { GM_deleteValue(key); } // Write the last_ran_purge_datestamp to the database var current_datestamp = build_current_datestamp(); GM_setValue("last_ran_purge_datestamp",current_datestamp); // Inform the user we cleared successfully alert("Cleared Successfully!"); } } function upload_data (){ // Alert the user that we restored successfully alert("Before you restore your posts makes sure that all tumblr tabs are closed except this one! Restoring data while other tabs are open may cause unexpected results."); // Restoring data wipes out existing entries this is to avoid situations // like when you have a key of 20170101_123456 and another key of 20170202_123456 // (basically two entries for the same request id) if (confirm('Are you sure you want restore? Restoring will overwrite anything you have in the database currently!!!')) { var input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('id', 'files'); input.setAttribute('accept', '.csv'); input.style.cssText = "display:none"; // Click the files input button (see the cpanel section) input.click(); // Clear the database var keys = GM_listValues(); for (var key of keys) { GM_deleteValue(key); } // Create a new FileReader var reader=new FileReader(); // when the file element changes read in the file input.onchange = function() { const file = input.files[0]; reader.readAsText(file); }; // When the file is completely read in reader.onload = function() { // Create a variable to store the results var csv = reader.result; // Create an array of lines by splitting up the csv on carraige returns var allTextLines = csv.split(/\r\n|\n/); // Create a variable to store the lines var lines = []; // From 0 to the total number of lines for (var i=0; i