Tumblr Savior

Saves you from ever having to see another post about certain things ever again (idea by bjornstar, rewritten by Vindicar).

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              Tumblr Savior
// @namespace         bjornstar
// @description       Saves you from ever having to see another post about certain things ever again (idea by bjornstar, rewritten by Vindicar).
// @version           3.1.4
// @require 	      https://greasyfork.org/scripts/1884-gm-config/code/GM_config.js?version=4836
// @run-at            document-start
// @grant             unsafeWindow
// @grant             GM_getValue
// @grant             GM_setValue
// @grant             GM_registerMenuCommand
// @grant             GM_addStyle
// @include           http://www.tumblr.com/*
// @include           https://www.tumblr.com/*
// ==/UserScript==

(function(){
	'use strict';
	//preparing the config file
	//If we have no access to Greasemonkey methods, we will need dummy replacements
	if (typeof GM_getValue !== 'function') GM_getValue = function (target, deflt) { return deflt; };
	// >>> YOU CAN SPECIFY DEFAULT VALUES BELOW <<<
	var cfg = {
		//posts matching black list will be hidden completely
		blacklist : parseList(GM_getValue('blacklist', '')),
		//posts matching gray list will be hidden under spoiler and can be revealed with a single click
		graylist  : parseList(GM_getValue('graylist', '')),
		//posts matching white list will never be affected by black or gray lists.
		whitelist : parseList(GM_getValue('whitelist', '')),
		//which action to take against sponsored posts
		sponsored_action : GM_getValue('sponsored', '0'),
		//which action to take against recommended posts
		recommended_action : GM_getValue('recommended', '0'),
		//if set to true, black and white lists will affect notifications and post notes as well.
		process_notifications_and_notes : GM_getValue('notes', false),
		//if true, script will search post HTML for triggerwords instead of it's visible text content
		search_html : GM_getValue('inhtml', false),
		// settings below this point are internal and have no GUI
		//if post removal should simply hide it
		soft_removal : true,
		post_selector : 'li.post_container:not(#new_post_buttons)',
		notification_selector : 'li.notification',
		note_selector : '.notes li.note',
		post_body_selector : '.post_body',
		//constants for the sake of code simplicity
		actions : {
			PROCESS		: '0',
			WHITELIST	: '1',
			HIDE		: '2',
			SPOILER		: '3',
			REMOVE		: '4',
		},
	};
	//=======================================================================
	//Main Tumblr Saviour object (maybe it's a god-object antipattern, I don't care)
	//=======================================================================
	var TumblrSaviour = { 
		config : cfg, //configuration object from above
		//helper function that looks up keywords from supplied array in a string and returns array of found keywords
		findKeywords : function (data, list) {
			var result = [];
			for (var i = 0; i < list.length; i++)
				if (data.indexOf(list[i]) >= 0)
					result.push(list[i]);
			return result;
		},
		//helper function that strips specified attributes from the root element and all its descendants
		stripAttrs : function (root, attrs) {
			//make sure we got an array
			if (typeof attrs == 'undefined')
				attrs = [];
			else if (typeof attrs == 'string')
				attrs = [attrs];
			for (var a=0; a<attrs.length; a++) {
				//stripping the node itself
				if (root.hasAttribute(attrs[a]))
					root.removeAttribute(attrs[a]);
				//finding all descendants that have this attribute
				var nodes = root.querySelectorAll('*['+attrs[a]+']');
				//and stripping them all
				for (var i=0; i<nodes.length; i++)
					nodes[i].removeAttribute(attrs[a]);
			}
		},
		//helper function that removes all matching descendants of the root element
		stripNodes : function (root, items) {
			if (typeof items == 'string')
				items = [items];
			for (var i=0; i<items.length; i++) {
				var nodes = root.querySelectorAll(items[i]);
				for (var j=0; j<nodes.length; j++)
					nodes[j].parentNode.removeChild(nodes[j]);
			}
		},
		//converts a post element into string for keyword lookup
		extractPostData : function (post) {
			var data = '';
			var clone = post.cloneNode(true);
			this.stripNodes(clone, ['script', '.post_footer']);
			if (this.config.search_html) {
				//these attributes may have fragments of text from blog description, which can lead to false positives
				this.stripAttrs(clone, ['data-tumblelog-popover', 'data-json']);
				data = clone.innerHTML;
			} else {
				recoursiveWalk(clone, function(el) {
					if (el.nodeType == el.TEXT_NODE)
						data += ' '+el.nodeValue;
				});
			}
			data = data.toLowerCase();
			return data;
		},
		//converts a notification element into string for keyword lookup
		extractNotificationData : function (notification) {
			return this.extractPostData(notification);
		},
		//converts a note element into string for keyword lookup
		extractNoteData : function (note) {
			return this.extractPostData(note);
		},
		//post and notification processing routines - they do actual work of hiding/removing posts
		//returns a previous non-blacklisted post element or null
		getPreviousPost : function (post) {
			var prev = post.previousSibling;
			while ( (prev !== null) && ( (prev.nodeType != 1) || (prev.querySelector('.post:not(.new_post)') === null) ) )
				prev = prev.previousSibling;
			return prev;
		},
		//returns post author name or empty string
		getPostAuthor : function (post) {
			if (post === null) return '';
			var actual_post = post.querySelector('.post');
			if (actual_post !== null)
				return actual_post.getAttribute('data-tumblelog');
			else
				return '';
		},
		//if there are several posts from the same author in a row, all but first will have "same_user_as_last" class applied.
		//back in the day such posts had their author icon hidden
		//currently it seems to be unused, but we will adjust the class nonetheless
		adjustPost : function (post) {
			var actual_post = post.querySelector('.post');
			if (actual_post === null) return; //is it even a post?
			var prev = this.getPreviousPost(post); //look up the previous one
			if (prev === null) {
				//we're dealing with the first visible post on dashboard - just make sure it has no "same user" class applied
				actual_post.className = actual_post.className.replace(/\bsame_user_as_last\b/, '');
			} else {
				//there is a previous post - let's check the authors
				if (this.getPostAuthor(post) == this.getPostAuthor(prev))
					//same author - setting the class
					actual_post.className += ' same_user_as_last'; 
				else
					//different authors - removing the class
					actual_post.className = actual_post.className.replace(/\bsame_user_as_last\b/, '');
			}
		},
		//for those who didn't trigger any list
		ignorePost : function (post, reason) {
			post.setAttribute('data-tumblr-saviour-status', 'unaffected');
			post.setAttribute('data-tumblr-saviour-reason', reason);
			this.adjustPost(post);
		},
		ignoreNotification : function (notification, reason) {
			notification.setAttribute('data-tumblr-saviour-status', 'unaffected');
			notification.setAttribute('data-tumblr-saviour-reason', reason);
		},
		//for those that triggered whitelist
		whiteListPost : function (post, reason) {
			post.setAttribute('data-tumblr-saviour-status', 'whitelisted');
			post.setAttribute('data-tumblr-saviour-reason', reason);
			this.adjustPost(post);
		},
		whiteListNotification : function (notification, reason) {
			notification.setAttribute('data-tumblr-saviour-status', 'whitelisted');
			notification.setAttribute('data-tumblr-saviour-reason', reason);
		},
		whiteListNote : function (note, reason) {
			note.setAttribute('data-tumblr-saviour-status', 'whitelisted');
			note.setAttribute('data-tumblr-saviour-reason', reason);
		},
		//for those that triggered graylist
		hidePostSpoiler : function (post, reason) {
			post.setAttribute('data-tumblr-saviour-status', 'graylisted');
			post.setAttribute('data-tumblr-saviour-reason', reason);			
			var content = post.querySelector('.post_content_inner');
			var contentstyle = content.style.display;
			content.style.display = 'none';
			if (!content) return;
			var placeholder = document.createElement('div');
			placeholder.className = 'tumblr_saviour_placeholder';
			placeholder.innerHTML = '<span>You have been saved from this post because of: '+reason+'. </span>';
			var trigger = document.createElement('span');
			trigger.innerHTML = '[<span class="tumblr_saviour_trigger">Show</span>]';
			placeholder.appendChild(trigger);
			content.parentNode.insertBefore(placeholder, content);
			trigger.addEventListener('click', function(e) {
				e.preventDefault();
				content.style.display = contentstyle;
				placeholder.style.display = 'none';
				placeholder.parentNode.removeChild(placeholder);
			});
			this.adjustPost(post);
		},
		//for those that triggered blacklist
		hidePost : function (post, reason) {
			//soft removal - just hiding the post
			post.setAttribute('data-tumblr-saviour-status', 'blacklisted');
			post.setAttribute('data-tumblr-saviour-reason', reason);			
			post.style.display = 'none';
			//we have to strip it of "post" class to ensure that keyboard navigation won't see it
			var actual_post = post.querySelector('.post');
			if (actual_post !== null)
				actual_post.className = actual_post.className.replace(/\bpost\b/, '');
			//we should tell Tumblr to update it's keyboard navigation, if possible
			checkIfExists('Tumblr.KeyCommands.update_post_positions', function (update_post_positions) {
				try {
					update_post_positions();
				} catch (e) {
					//we ignore any errors that might have happened
				}
			});
		},
		removePost : function (post, reason) {
			post.parentNode.removeChild(post);
			//we should tell Tumblr to update it's keyboard navigation, if possible
			checkIfExists('Tumblr.KeyCommands.update_post_positions', function (update_post_positions) {
				try {
					update_post_positions();
				} catch (e) {
					//we ignore any errors that might have happened
				}
			});
		},
		hideNotification : function (notification, reason) {
			notification.setAttribute('data-tumblr-saviour-status', 'blacklisted');
			notification.setAttribute('data-tumblr-saviour-reason', reason);			
			notification.style.display = 'none';
		},
		removeNotification : function (notification, reason) {
			notification.parentNode.removeChild(notification);
		},
		removeNote : function (note, reason) {
			if (this.config.soft_removal) {
				note.setAttribute('data-tumblr-saviour-status', 'blacklisted');
				note.setAttribute('data-tumblr-saviour-reason', reason);
				note.style.display = 'none';
			} else {
				note.parentNode.removeChild(note);
			}			
		},
		//post and notification analysis routines - in case Tumblr changes something
		isMyPost : function (post) {
			return (post.querySelector('.not_mine') === null);
		},
		isSponsoredPost : function (post) {
			return (post.querySelector('.sponsored_post') !== null);
		},	
		isSponsoredNotification : function (notification) {
			return (notification.querySelector('.sponsor') !== null);
		},
		isRecommendedPost : function (post) {
			return (post.querySelector('.is_recommended') !== null) || (post.querySelector('.recommendation-reason-footer') !== null);
		},
		isRecommendedNotification : function (notification) {
			return checkSelectorMatch(notification,'.takeover-container');
		},
		//main post analysis routine
		analyzePost : function (post) {
			if (this.isMyPost(post)) {
				//user's own posts are always whitelisted
				this.whiteListPost(post, 'my post');
				return;
			}
			//check if it's a sponsored post
			if (this.isSponsoredPost(post))
				switch (this.config.sponsored_action){
					case this.config.actions.WHITELIST: {
						this.whiteListPost(post,'sponsored post');
						return;
					}; break;
					case this.config.actions.HIDE: {
						this.hidePost(post,'sponsored post');
						return;
					}; break;
					case this.config.actions.REMOVE: {
						this.removePost(post,'sponsored post');
						return;
					}; break;
					case this.config.actions.SPOILER: {
						this.hidePostSpoiler(post,'sponsored post');
						return;
					}; break;
					default: break;
				}
			//check if it's a recommended post
			if (this.isRecommendedPost(post))
				switch (this.config.recommended_action){
					case this.config.actions.WHITELIST: {
						this.whiteListPost(post,'recommended post');
						return;
					}; break;
					case this.config.actions.HIDE: {
						this.hidePost(post,'recommended post');
						return;
					}; break;
					case this.config.actions.REMOVE: {
						this.removePost(post,'recommended post');
						return;
					}; break;
					case this.config.actions.SPOILER: {
						this.hidePostSpoiler(post,'recommended post');
						return;
					}; break;
					default: break;
				}
			//white list takes priority
			var data = this.extractPostData(post);
			var keywords;
			keywords = this.findKeywords(data, this.config.whitelist);
			if (keywords.length) {
				this.whiteListPost(post, keywords.join(';'));
				return;
			}
			//black list
			keywords = this.findKeywords(data, this.config.blacklist);
			if (keywords.length) {
				this.hidePost(post, keywords.join(';'));
				return;
			}
			//check the gray list
			keywords = this.findKeywords(data, this.config.graylist);
			if (keywords.length) {
				this.hidePostSpoiler(post, keywords.join(';'));
				return;
			}
			//if nothing triggered, we mark post as such
			this.ignorePost(post, '');
		},
		//main notification analysis routine
		analyzeNotification : function (notification) {
			if (this.config.process_notifications_and_notes) {
				var data = this.extractNotificationData(notification);
				var keywords;
				keywords = this.findKeywords(data, this.config.whitelist);
				if (keywords.length) {
					this.whiteListNotification(notification, keywords.join(';'));
					return;
				}
				keywords = this.findKeywords(data, this.config.blacklist);
				if (keywords.length) {
					this.hideNotification(notification, keywords.join(';'));
					return;
				}
				if (this.isSponsoredNotification(notification))
					switch (this.config.sponsored_action){
						case this.config.actions.WHITELIST: {
							this.whiteListNotification(notification,'sponsored notification');
							return;
						}; break;
						case this.config.actions.SPOILER:
						case this.config.actions.HIDE: {
							this.hideNotification(notification,'sponsored notification');
							return;
						}; break;
						case this.config.actions.REMOVE: {
							this.removeNotification(notification,'sponsored notification');
							return;
						}; break;
						default: break;
					}
				if (this.isRecommendedNotification(notification))
					switch (this.config.recommended_action){
						case this.config.actions.WHITELIST: {
							this.whiteListNotification(notification,'recommended notification');
							return;
						}; break;
						case this.config.actions.SPOILER:
						case this.config.actions.HIDE: {
							this.hideNotification(notification,'recommended notification');
							return;
						}; break;
						case this.config.actions.REMOVE: {
							this.removeNotification(notification,'recommended notification');
							return;
						}; break;
						default: break;
					}
			}
			this.ignoreNotification(notification,'');
		},
		//main note analysis routine
		analyzeNote : function (note) {
			if (this.config.process_notifications_and_notes) {
				var data = this.extractNoteData(note);
				var keywords;
				keywords = this.findKeywords(data, this.config.whitelist);
				if (keywords.length) {
					this.whiteListNote(note, keywords.join(';'));
					return;
				}
				keywords = this.findKeywords(data, this.config.blacklist);
				if (keywords.length) {
					this.removeNote(note, keywords.join(';'));
					return;
				}
			}
		},
	};
	//=======================================================================
	//Function definitions (don't worry, JS will lift them to the beginning of the block)
	//=======================================================================
	//iterate through the node's descendants
	function recoursiveWalk(element, fn) {
		if (!fn(element) && (element.nodeType == element.ELEMENT_NODE))
			for (var i=0; i<element.childNodes.length; i++)
				recoursiveWalk(element.childNodes[i], fn);
	}
	//parsing semicolon-separated lists into sorted arrays
	function parseList(list) {
		var lst = list.split(';');
		var res = [];
		for (var i=lst.length-1;i>=0;i--) {
			if (lst[i].trim().length>0)
				res.push(lst[i].toLowerCase());
		}
		res.sort();
		return res;
	}
	//helper function that checks if specified object hierarchy exists in the page scope and returns boolean flag/runs a callback if it does.
	function checkIfExists(objects, callback) {
		if (typeof objects === 'string')
			objects = objects.split('.');
		var obj = unsafeWindow;
		for (var index = 0; index<objects.length; index++) {
			if (typeof obj[objects[index]] === 'undefined') 
				return false;
			else
				obj = obj[objects[index]];
		}
		if (typeof callback !== 'undefined')
			callback(obj);
		return true;
	}
	//helper function to determine if specified node matches specified selector
	function checkSelectorMatch(node, selector) {
		if (typeof node[checkSelectorMatch.method] == 'function') //not all nodes have required methods
			return node[checkSelectorMatch.method](selector);
		else //in that case, we simply assume it doesn't match
			return false;
	}
	//determining matching method supported by the browser
	checkSelectorMatch.method = (function(){ 
		var methods = ['matches', 'matchesSelector', 'mozMatchesSelector', 'webkitMatchesSelector'];
		for (var i=0; i<methods.length; i++)
			if (typeof Element.prototype[methods[i]] == 'function')
				return methods[i]; //match found, remember it for future use
		throw "No way to match selector found."; //no match - we have to fail miserably.
	})();
	
	//waits for a node specified by selector to appear/disappear
	function waitForSelector(selector, must_exist, callback, root) { 
		if (typeof root == 'undefined') //we search the whole document unless told otherwise
			root = document;
		//we check if the node has been added/removed already
		var prequery = root.querySelector(selector);
		if ( (prequery !== null) == must_exist ) {
			callback(prequery);
			return;
		}
		//it hasn't - we set up MutationObserver on root element to find it
		var mutation_callback = function(mutations) {
			//checking the list of mutations
			for (var i=0; i<mutations.length; i++) {
				//make sure the event is of correct type
				if (mutations[i].type == 'childList')
					if (must_exist) { //we're waiting for the node to appear, so we look for added nodes that match our selector
						for (var j=0; j<mutations[i].addedNodes.length; j++)
							if (checkSelectorMatch(mutations[i].addedNodes[j], selector)) 
								try {
									callback(mutations[i].addedNodes[j]);
								} 
								finally {
									mutation_callback.docobserver.disconnect();
									delete mutation_callback.docobserver;
									return;
								}
					} else { //we're waiting for the node to disappear, so we look for removed nodes that match our selector
						for (var j=0; j<mutations[i].removedNodes.length; j++)
							if (checkSelectorMatch(mutations[i].removedNodes[j], selector)) 
								try {
									callback(mutations[i].removedNodes[j]);
								} 
								finally {
									mutation_callback.docobserver.disconnect();
									delete mutation_callback.docobserver;
									return;
								}
					}
			}
		};
		mutation_callback.docobserver = new MutationObserver(mutation_callback);
		mutation_callback.docobserver.observe(root, { 
			attributes: false, 
			childList: true, 
			characterData: false, 
			subtree: true,
		});
	}

	//=======================================================================
	//Main script
	//=======================================================================
	//we prepare the DOM observers
	//observer for any new posts coming up
	var new_post_observer = new MutationObserver(function(mutations){
		for (var i=0; i<mutations.length; i++) { //looking through mutations list
			if (mutations[i].type == 'childList') 
				for (var j = 0; j<mutations[i].addedNodes.length; j++) { //only checking additions
					//is it a post?
					if (checkSelectorMatch(mutations[i].addedNodes[j], TumblrSaviour.config.post_selector)) {
						TumblrSaviour.analyzePost.call(TumblrSaviour, mutations[i].addedNodes[j]);
						post_update_observer.observe(mutations[i].addedNodes[j], post_update_observer_config);
					}
					//is it a notification?
					else if (checkSelectorMatch(mutations[i].addedNodes[j], TumblrSaviour.config.notification_selector))
						TumblrSaviour.analyzeNotification.call(TumblrSaviour, mutations[i].addedNodes[j]);
				}
		}
	});
	//configuration: interested only in immediates descendants being added/removed 
	var new_post_observer_config = {
		attributes: false, 
		childList: true, 
		characterData: false, 
		subtree: false,
	};
	//some post don't have post body initially - we have to schedule a check later.
	var post_update_observer = new MutationObserver(function(mutations){
		for (var i=0; i<mutations.length; i++) //looking through mutations list
			for (var j=0; j<mutations[i].addedNodes.length; j++)
				if (checkSelectorMatch(mutations[i].addedNodes[j], TumblrSaviour.config.post_body_selector)) {
					//looking for post node containing this body
					var node = mutations[i].addedNodes[j];
					while ((node !== null) && !checkSelectorMatch(node, TumblrSaviour.config.post_selector))
						node = node.parentNode;
					if (node !== null) //post node found
						TumblrSaviour.analyzePost.call(TumblrSaviour, node);
				}
	});
	//configuration: interested in any changes to DOM tree
	var post_update_observer_config = {
		attributes: false,
		childList: true, 
		characterData: false, 
		subtree: true,
	};
	//we wait for #posts to appear in the DOM tree
	waitForSelector('#posts', true, function(posts){
		//we immediately set an observer on it, so we can catch not-yet-loaded posts, as well as ones added dynamically by the paginator
		new_post_observer.observe(posts, new_post_observer_config);		
		//then we check for items already loaded
		var notifylist = posts.querySelectorAll(TumblrSaviour.config.notification_selector);
		for (var i=0; i<notifylist.length; i++)
			TumblrSaviour.analyzeNotification.call(TumblrSaviour, notifylist[i]);
		var postlist = posts.querySelectorAll(TumblrSaviour.config.post_selector);
		for (var i=0; i<postlist.length; i++)
			//some posts don't initially have a body
			if (postlist[i].querySelector(TumblrSaviour.config.post_body_selector) !== null)
				//if they do, we check them immediately
				TumblrSaviour.analyzePost.call(TumblrSaviour, postlist[i]);
			else
				//if they don't, we observe them so they will get checked once it appears
				post_update_observer.observe(postlist[i], post_update_observer_config);
	});
	//if we want to filter post notes, we will have to get our hands dirty
	if (TumblrSaviour.config.process_notifications_and_notes) {
		//once document is loaded and Tumblr scripts have been set up, we set a hook to catch the moment post notes are being loaded.
		window.addEventListener("load", function() {
			//we remember old function that handles notes loading 
			var old_load_notes = unsafeWindow.Tumblr.Notes.prototype.load_notes;
			//and replace it with ours
			unsafeWindow.Tumblr.Notes.prototype.load_notes = exportFunction(function($post,options,fn){
				//the idea is to allow Tumblr engine to load notes...
				old_load_notes.call(this, $post, options, exportFunction(function(data){
					//...and render those notes...
					var res = fn(data);
					//...but also to filter them immediately afterwards
					var notes = $post[0].querySelectorAll(TumblrSaviour.config.note_selector);
					for (var i=0; i<notes.length; i++)
						TumblrSaviour.analyzeNote.call(TumblrSaviour, notes[i]);
					return res;
				}, unsafeWindow));
			}, unsafeWindow);
		});
	}
	//we set up the configuration panel if possible
	if ( (typeof GM_config !== 'undefined') && (typeof GM_setValue === 'function') && (typeof GM_registerMenuCommand === 'function') ) {
		var fields = {
			"blacklist" : {
				"label" : "Blacklisted words",
				"title" : "Semicolon-separated list of words that will cause the post to disappear.",
				"type" : "text",
				"default" : GM_getValue('blacklist', ''),
			},
			"graylist" : {
				"label" : "Graylisted words",
				"title" : "Semicolon-separated list of words that will cause the post content to be hidden under spoiler.",
				"type" : "text",
				"default" : GM_getValue('graylist', ''),
			},
			"whitelist" : {
				"label" : "Whitelisted words",
				"title" : "Semicolon-separated list of words that will prevent post from being hidden for any reason. Your own posts are always whitelisted.",
				"type" : "text",
				"default" : GM_getValue('whitelist', ''),
			},
			"sponsored" : {
				"label" : "Action for sponsored posts",
				"title" : "If set to anything but 'process like any other post', this setting overrides the effect of lists above.",
				"type" : "select",
				"options" : {
					"0" : "process like any other post",
					"1" : "whitelist post",
					"2" : "blacklist post",
					"3" : "hide post under spoiler",
					"4" : "remove from the page",
				},
				"default" : GM_getValue('sponsored', '0'),
			},
			"recommended" : {
				"label" : "Action for recommended posts",
				"title" : "If set to anything but 'process like any other post', this setting overrides the effect of lists above.",
				"type" : "select",
				"options" : {
					"0" : "process like any other post",
					"1" : "whitelist post",
					"2" : "blacklist post",
					"3" : "hide post under spoiler",
					"4" : "remove from the page",
				},
				"default" : GM_getValue('recommended', '0'),
			},
			"notes" : {
				"label" : "Process notifications and notes as well",
				"type" : "checkbox",
				"default" : !!GM_getValue('notes', 0),
			},
			"inhtml" : {
				"label" : "Check HTML code of the post instead of its text",
				"type" : "checkbox",
				"default" : !!GM_getValue('inhtml', 0),
			},
			save: function() {
				GM_config.values['blacklist'] = parseList(GM_config.values['blacklist']).join(";");
				GM_config.values['graylist'] = parseList(GM_config.values['graylist']).join(";");
				GM_config.values['whitelist'] = parseList(GM_config.values['whitelist']).join(";");
				for (var key in GM_config.values)
					GM_setValue(key,GM_config.values[key]);
			},
		};
		var CSS = [
			'.section_header,.reset_holder { display: none !important; }',
			'body {background-color: #FFF;}',
			'* {font-family: "Helvetica Neue","HelveticaNeue",Helvetica,Arial,sans-serif; color: #444;}',
			'#header {border-bottom: 2px solid #E5E5E5; font-size: 24px; font-weight: normal; line-height: 1; margin: 0px; padding-bottom: 28px;}',
			'.config_var {padding: 2px 0px 2px 200px;}',
			'.config_var>* {vertical-align:middle;}',
			'.config_var .field_label {font-size: 14px !important;line-height: 1.2; display:inline-block; width:200px; margin: 0 0 0 -200px;}',
			'#field_blacklist,#field_graylist,#field_whitelist {width: 100%}',
			'button {padding: 4px 7px 5px; font-weight: 700; border-width: 1px; border-style: solid; text-decoration: none; border-radius: 2px; cursor: pointer; display: inline-block; height: 30px; line-height: 20px;}',
			'#saveBtn {color: #FFF; border-color: #529ECC; background: #529ECC none repeat scroll 0% 0%;}',
			'#cancelBtn {color: #FFF; border-color: #9DA6AF; background: #9DA6AF none repeat scroll 0% 0%;}',
			""].join("\n");
		GM_addStyle([
			'#GM_config {border-radius: 3px !important; border: 0px none !important;}',
			'.tumblr_saviour_placeholder { display: block; padding: 20px;}',
			'.tumblr_saviour_trigger { cursor: pointer !important; text-decoration: underline !important; }',
		""].join("\n"));
		GM_config.init("Tumblr Saviour Settings", fields, CSS);
		GM_registerMenuCommand("Tumblr Saviour Settings", function() {GM_config.open();});
	}

})();