There are 8 previous versions of this script.
Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)
// ==UserScript==
// @name deviantTIDY
// @namespace http://boffinbrain.deviantart.com
// @description Performs a variety of functions on deviantART pages to improve its look and usability. deviantTIDY also consists of a stylesheet. For full details, see http://www.deviantart.com/deviation/45622809/
// @version 4.0.3
// @include http*.deviantart.com/*
// ==/UserScript==
// deviantART body element
var dA = $('deviantART-v6-1');
// Get a DOM element by its ID
function $(id) {return document.getElementById(id);}
// Create a DOM text node
function $S(str) {return document.createTextNode(str);}
// Remove a DOM element
function $R(e) {if(e && e.parentNode) e.parentNode.removeChild(e);}
// Create a DOM element (tag, [properties,] children)
function $E()
{
if(arguments.length==0) return;
function applyObj(to, obj)
{
for(prop in obj) if(obj.hasOwnProperty(prop))
{
if(typeof(obj[prop]) == 'object')
{
applyObj(to[prop], obj[prop]);
}
else
{
to[prop] = obj[prop];
}
}
}
var elm = document.createElement(arguments[0]);
[arguments[1], arguments[2]].forEach(function(arg, idx, ary)
{
if(typeof(arg)=='object')
{
if(arg instanceof Array)
{
// Child elements
arg.forEach(function(append, idx, ary)
{
elm.appendChild((typeof(append)=='string') ? $S(append) : append);
});
}
else
{
// Properties of element
for(prop in arg) if(arg.hasOwnProperty(prop))
{
if(prop=='events')
{
// Add event listeners
var events = arg[prop];
for(evt in events) if(events.hasOwnProperty(evt))
{
elm.addEventListener(evt.replace(/^on/,''), events[evt], false);
}
}
else
{
// Add normal properties
if(typeof(arg[prop])=='object')
{
applyObj(elm[prop], arg[prop]);
}
else
{
elm[prop]=arg[prop];
}
}
}
}
}
});
return elm;
}
// Standard XPATH shorthand function
function $X(query, node)
{
try
{
return document.evaluate(query, node?node:document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
catch(e)
{
try
{
return content.document.evaluate(query, node?node:content.document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
catch(e)
{
GM_log("Error in XPATH query");
}
}
}
// The deviantTIDY-specific modal interface
var deviantdialog =
{
identifier: 'devianttidy-dialog',
open: function(header, body)
{
// If dialog is open, close it and start a new one
if($(this.identifier)) this.close();
if(typeof header != 'string') return;
if(typeof body == 'string') body = [$E('div', {className:'ppp c'}, [body])];
var popup = $E('div', {id:this.identifier, style:{display:'none'}}, [
$E('div', [
$E('div', {className:'gr-box gr-genericbox'}, [
$E('i', {className:'gr1'}),
$E('i', {className:'gr2'}),
$E('i', {className:'gr3'}),
$E('div', {className:'gr-top'}, [
$E('i', {className:'tri'}),
$E('div', {className:'gr'}, [
$E('h2', [$E('a', {href:devianttidy.url, title:'deviantTIDY Homepage'}, [
$E('img', {className:'dialog-icon', src:devianttidy.icons.dt})]),
header
]),
$E('img', {className:'dialog-close', src:devianttidy.icons.close, events:{'click':function(){deviantdialog.close();}}})
])
]),
$E('div', {className:'gr-body'}, [
$E('div', {className:'ppp'}, body)
]),
$E('i', {className:'gr3 gb'}),
$E('i', {className:'gr2 gb'}),
$E('i', {className:'gr1 gb gb1'})
])
])
]);
dA.appendChild(popup);
var gr = popup.childNodes[0];
gr.style.marginTop = (gr.offsetHeight ? -gr.offsetHeight/2 : -250)+"px";
// Display warning about lack of CSS. The warning is hidden by the CSS when loaded.
dA.appendChild($E('div', {
id:'devianttidy-no-css',
style:{
position:'fixed', top:'50%', left:'20%', right:'20%', zIndex:300,
background:'#fed', textAlign:'center', padding:'1em'
},
events:{'click':function(){$R($('devianttidy-no-css'));}}
}, [
'deviantTIDY CSS not loaded!', $E('br'), 'To get the CSS, install ',
$E('a', {href:"https://addons.mozilla.org/en-US/firefox/addon/2108"}, ['Stylish']),
' and go to the ', $E('a', {href:devianttidy.url}, ['deviantTIDY Homepage']),
' to grab the stylesheet.'
]
));
},
close: function()
{
$R($(this.identifier));
}
};
// The deviantTIDY application
var devianttidy =
{
version: '4.0.3',
debug: false,
extension: false,
url: 'http://www.deviantart.com/deviation/45622809/',
dialog: deviantdialog,
icons:
{
dt: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAACB0RVh0U29mdHdhcmUATWFjcm9tZWRpYSBGaXJld29ya3MgTVi7kSokAAAAFnRFWHRDcmVhdGlvbiBUaW1lADA1LzI1LzA5eCfTMQAAAPJJREFUeJzllD2KQkEQhL8SE4UNvIEnMBNTYSM9x7KHMDbyAkYaGnkHAxEWI28gwmK2ZsZtoGj7GN4vA8oWdDDTPVXM1HTLzIiJWlT2/yMgaSppI2lfVEB5TJZ0LzIzFRGI/kT10KakHjAAusCwkoKZ3QPoAzPAbvEHLNzabnVbYAl8+/Oh8OQjTwRMXC4p8LTOFAC+EuTjxM3KCwBt4OQO/QaKSgvUgTnQcrY0JK14/LB2moeSTsAHcAyk1wLOQDONxMPM5PsiA4casMtLXgI/uTq5CoKNloUio+M1pmkVlHoioAN8ch0tqYhu8vt7EF3gAq5S40Q1cBgMAAAAAElFTkSuQmCC',
close: 'data:image/gif;base64,R0lGODlhDwAPAJEAAP///9vg2kdSS////yH5BAUUAAMALAAAAAAPAA8AAAIrnC2Zx6O/GJxnWpRAUAEox2lCt1mjJpoJqa5oabHsp6TnB7ZC1TZqw8MdCgA7'
},
log: function(message)
{
if(this.debug) GM_log(message);
},
start: function()
{
if(!dA)
{
GM_log("deviantTIDY only works on v6.1 pages.");
return;
}
if(unsafeWindow.devianttidy)
{
alert("Another instance of deviantTIDY has already loaded! Please check your installed scripts and extensions.");
return;
}
this.manifest();
this.dispatch();
},
manifest: function()
{
// Fresh update?
if(GM_getValue('version') != this.version)
{
GM_setValue('version', this.version);
devianttidy.dialog.open('deviantTIDY '+this.version+' Installed', [
$E('div', {className:'pp'}, ["Congratulations; deviantTIDY is installed and running!"]),
$E('div', {className:'pp'}, [
"You can view all available options by clicking 'deviantTIDY' on the footer of any deviantART page, or ",
$E('a', {href:"javascript:;", className:'a', events:{click:devianttidy.preferences}}, ["set your preferences right away"]),
"."
]),
$E('div', {className:'pp'}, ["You will not see this message again. Close this panel to continue browsing."]),
]);
}
// Silently look for updates every 2 days - alert user if new version is available
var now = new Date();
var last = GM_getValue('last_updated');
if(!last || Date.parse(last).valueOf() < now - 48*3600*1000)
{
this.update(true);
GM_setValue('last_updated', now.toString());
}
// Add Greasemonkey menu item
GM_registerMenuCommand("deviantTIDY Options", this.preferences);
// Add the Options link to the page footer
var depths = $X("//div[@id='depths']//div[@class='footer_tx_links']").snapshotItem(0);
if(depths)
{
depths.insertBefore($S(' | '), depths.childNodes[0]);
depths.insertBefore($E('a', {
id:'devianttidy-options-link',
href:'javascript:;',
events:{click: this.preferences}
}, ['deviantTIDY']), depths.childNodes[0]);
}
// Allow this application and its dialog manager to be accessed by other scripts through unsafeWindow
unsafeWindow.devianttidy = this;
},
preferences: function()
{
var controls = [];
// Generate options controls
for(var o in devianttidy.options)
{
// Options without descriptions are hidden functions, but their preferences can be set manually
option = devianttidy.options[o];
if(option.description)
{
var control;
var control_id = 'devianttidy-control-'+o;
var description = [option.description, $E('b', [(option.custom ? ' (add-on)' : '')])];
if(option.choices)
{
// If a list of choices is provided, the options are radio buttons
control = [$E('div', description)];
for(var c=0; c<option.choices.length; c++)
{
control.push($E('div', [
$E('input', {type:'radio', id:control_id+c, name:o, value:c, checked:(option.pref==c), events: {
change: function(){devianttidy.options[this.name].pref = this.value;}
}}),
$E('label', {htmlFor:control_id+c}, [option.choices[c]])
]));
}
}
else
{
// Otherwise, use a checkbox
control = [
$E('input', {type:'checkbox', id:control_id, name:o, checked:option.pref, events: {
change: function(){devianttidy.options[this.name].pref = this.checked ? 1 : 0;}
}}),
$E('label', {htmlFor:control_id}, description),
];
}
if(option.hint)
{
control.push($E('div', {className:'hint'}, [option.hint]));
}
controls.push($E('div', {className:'pp dialog-control'}, control));
}
}
devianttidy.dialog.open('Options',[
$E('div', {className:'p r'}, [
$E('a', {href:devianttidy.url}, ["deviantTIDY"]),
' version '+devianttidy.version+'. ',
$E('a', {href:"javascript:;", events:{'click':function() {devianttidy.update();}}}, ["Look for updates"])
]),
$E('div', {className:'pp dialog-controls'}, controls),
$E('div', {className:'dialog-buttons'}, [
$E('button', {events:{'click':function() {devianttidy.save(); devianttidy.reload()}}}, ['Save']),
$E('button', {events:{'click':function() {if(devianttidy.reset()) devianttidy.reload();}}}, ['Reset']),
$E('button', {events:{'click':function() {devianttidy.dialog.close();}}}, ['Cancel'])
])
]);
},
load: function()
{
for(var o in this.options) {this.options[o].pref = GM_getValue(o, this.options[o].initial);}
this.log("Loaded options");
},
save: function()
{
for(var o in this.options) {GM_setValue(o, this.options[o].pref);}
this.log("Saved options");
},
reset: function()
{
if(confirm("This will reset all deviantTIDY options to their default values."))
{
for(var o in this.options) {this.options[o].pref = this.options[o].initial;}
this.save();
this.log("Reset options");
return true;
}
return false;
},
dispatch: function()
{
this.load();
this.log("Dispatching...");
var dispatch_start = new Date();
var dispatch_count = 0;
for(var o in this.options)
{
// A lazy option should only run when the pref is not 0.
if(this.options[o].lazy && this.options[o].pref == 0) continue;
// If dispatcher is run again, don't repeat functions that were already run.
if(this.options[o].dispatched) continue;
var dispatch_time = new Date();
var dispatch_result = this.options[o].method();
this.options[o].dispatched = true;
dispatch_count++;
this.log(" Dispatch '"+o+"' returned "+dispatch_result+" in "+(new Date() - dispatch_time)+"ms");
}
this.log("Dispatched "+dispatch_count+" function(s) in "+(new Date() - dispatch_start)+"ms");
},
reload: function()
{
devianttidy.dialog.open('Reloading...', "Reloading page. Please wait...");
location.reload();
},
update: function(quiet)
{
this.log("Looking for updates...");
var update_error = function()
{
if(!quiet)
{
devianttidy.dialog.open('Error', [
$E('div', {className:'ppp c'}, [
"Unable to get the version information from ",
$E('a', {href:devianttidy.url}, [devianttidy.url]),
". Please try visiting the page yourself to check for updates."
]),
]);
}
};
if(!quiet)
{
devianttidy.dialog.open('Looking for Updates...', "Checking for a new version of deviantTIDY. Please wait...");
}
GM_xmlhttpRequest
({
method: "GET",
url: this.url,
onload: function(response)
{
var version_text = response.responseText.match(/<b><\/b><b><i>version ([\d.]+)<\/i><\/b><b><\/b>/);
if(response.status == 200 && version_text)
{
var message;
var version_number = version_text[1];
if(version_text[1] == devianttidy.version)
{
devianttidy.log("No newer version available.");
if(quiet) return;
message = ["Your version of deviantTIDY is up to date!"];
}
else
{
devianttidy.log("Newer version available.");
if(devianttidy.extension)
{
message = [
"deviantTIDY "+version_number+" is available. ",
"Open your Firefox Add-Ons window and click Check For Updates."
];
}
else
{
message = [
"deviantTIDY "+version_number+" is available. ",
$E('a', {href:devianttidy.url}, ["Go to the deviantTIDY homepage"]),
" to update your style and script."
];
}
}
devianttidy.dialog.open('Update Status', [$E('div', {className:'ppp c'}, message)]);
}
else
{
update_error();
}
},
onerror: update_error
});
},
extend: function(object)
{
// Use this function to add your own options to deviantTIDY.
// Call unsafeWindow.devianttidy.extend(...) from a user script to add your custom routine.
// This will automatically load and execute the routine.
// Description and method (function) are required.
if(typeof object.name != 'string' || typeof object.method != 'function' || typeof object.description != 'string')
{
this.log("Attempt to extend with a malformed add-on");
alert(
"deviantTIDY doesn't like the structure of the option you tried to add.\n"+
"Please check that you have the required parameters and that their types are correct."
);
return;
}
else if(this.options[object.name])
{
this.log("Attempt to extend resulted in a name-clash.");
alert("The deviantTIDY option '"+object.name+"' already exists. You cannot extend it.");
return;
}
if(typeof object.initial == 'undefined') object.initial = 1;
object.custom = true;
this.options[object.name] = object;
this.log("Extended with custom function '"+object.name+"'");
// We must delegate this to a timeout because executing it under unsafeWindow will
// result in access violations when calling GM functions
window.setTimeout(function(){devianttidy.dispatch();}, 1);
},
options:
{
'short_titles':
{
description: "Shorten page titles by removing deviantART prefixes and suffixes",
hint: "For instance, a window or tab named 'Spyed on deviantART' will be shortened to 'Spyed'.",
initial: 1,
lazy: true,
method: function() {document.title = document.title.replace(/^(deviantART): where ART meets application!$|^deviantART: | on deviantART$| deviantART( \w+)$/i, '$1$2'); return 1;}
},
'scroll_comments':
{
description: "Add scrollbars to long comments and notes...",
initial: 2,
lazy: true,
choices: ["Never", "When comment is over 15 lines long", "When comment is over 30 lines long"],
method: function() {dA.className += " dt-scroll-comments s" + this.pref; return 1;}
},
'limit_width':
{
description: "Limit the maximum width of deviantART pages on wide screens",
initial: 0,
lazy: true,
choices: ['No limit', '1200 pixels', '1400 pixels', '1600 pixels'],
method: function() {dA.className += " dt-limit-width l"+this.pref; return 1;}
},
'no_moods':
{
description: "Hide all mood icons in comments and mood selectors in reply boxes",
initial: 0,
lazy: true,
method: function() {dA.className += " dt-no-moods"; return 1;}
},
'strip_outgoing_links':
{
description: "Strip deviantART redirects from external links",
initial: 1,
lazy: true,
method: function()
{
var links = dA.getElementsByTagName('a');
for (var i=0, j=0; i<links.length; i++)
{
var link = links[i];
if(link.href && link.href.indexOf('http://www.deviantart.com/users/outgoing?')==0)
{
link.href = link.href.substring(41);
}
}
return j;
}
},
'collapse_sidebar':
{
description: "Collapse the folders/categories sidebar on galleries and browse pages",
initial: 0,
lazy: true,
method: function() {dA.className += " dt-collapse-sidebar"; return 1;}
},
'redirect_on_login':
{
description: "After logging in...",
initial: 0,
lazy: true,
choices: ['Do not redirect', 'Go to your Message Center', 'Go to your Channels page, like dAv5'],
method: function()
{
if(location.href=="http://www.deviantart.com/?loggedin=1")
{
var redirects = {
1: {url:'http://my.deviantart.com/messages/', name:'Message Center'},
2: {url:'http://www.deviantart.com/channels/', name:'Channels'}
};
devianttidy.dialog.open("Logged In", "Going to "+redirects[this.pref].name+"...");
location.href = redirects[this.pref].url;
return 1;
}
return 0;
}
},
'no_prints':
{
description: "Skip the Prints stage when submitting deviations.",
initial: 1,
lazy: true,
method: function()
{
if(location.href.indexOf("http://www.deviantart.com/submit/sell/?ty=1&deviationId=")==0)
{
devianttidy.dialog.open("No Prints Please!", "Skipping straight to deviation...");
location.href = location.href.replace(/submit\/sell\/\?ty=1&deviationId=/, "deviation/");
return 1;
}
return 0;
}
},
'show_forum_icons':
{
description: "Show forum thread icons",
initial: 0,
lazy: true,
method: function() {dA.className += " dt-forum-icons"; return 1;}
},
'disable_dnd':
{
description: "Disable drag-and-drop thumbnail collecting",
hint: "Note that while drag-and-drop is disabled, you will be unable to perform actions like selecting and moving items in your messages or gallery.",
initial: 0,
lazy: true,
method: function() {if(unsafeWindow.DDD) {delete unsafeWindow.DDD; return 1;} else {return 0;}}
},
'divide_signatures':
{
description: "Dividing lines between comments and signatures",
initial: 1,
lazy: true,
method: function()
{
var time = new Date();
var boundary = "<br><br>--<br>";
var i, j=0, node, parts;
// Get comment nodes
var nodes = $X("//div[@class='thought block']/div[@class='body']|//div[contains(@class,'ccomment')]//div[@class='text text-ii']");
for (i=0; node=nodes.snapshotItem(i); i++) if(node.innerHTML.indexOf(boundary)>0)
{
// Get the comment and signature portions as 2-element array
parts = node.innerHTML.split(boundary);
if(parts.length > 2)
{
devianttidy.log("A comment was found with more than one signature boundary ("+(parts.length-1)+")");
continue;
}
else
{
node.innerHTML = parts[0] + '<div class="box dt-deviant-signature">' + parts[1] + '</div>';
j++;
}
}
return j;
}
},
'divide_notes':
{
description: "Dividing lines between individual replies in notes",
initial: 1,
lazy: true,
method: function()
{
if(!$("notes")) return -2;
node = $X("//div[@class='item']/div[@class='trailing item-body']/p").snapshotItem(0);
if(!node) return -1;
var divider = "<br>----------<br>";
var parts = node.innerHTML.split(divider);
if(parts.length>1) node.innerHTML = parts.join('<div class="box dt-deviant-signature"> </div>');
return parts.length-1;
}
},
'floating_comment':
{
initial: 1,
lazy: true,
method: function()
{
var link, form, box, close, body;
link = $X("//h2/a[@href='#commentbody']").snapshotItem(0); // v5 pages
if(!link) link = $X("//a[@href='#commentbody']", $('deviation-links')).snapshotItem(0); // v6 deviations
if(form = $('cooler-comment-submit')) box = form.parentNode.parentNode.parentNode;
else if(form = $('commentsubmit')) {box = form; box.childNodes[1].className += ' bubbleview';}
else return -2;
if(!(close = $X("//img[@class='cx']|//a[@class='x']", box).snapshotItem(0))) return -1;
body = $('commentbody');
var icon_down = 'data:image/gif;base64,R0lGODlhDwAPAKIAAP///9zh29vg2trf2UhTTEdSS0ZRSv///yH5BAEHAAcALAAAAAAPAA8AAAMyeKrVvfC4+SC962pFNJVeWAABYQpAdw0kgQrqSgICFTds/d3FgB28DUcUMdkIkcUtkgAAOw==';
var icon_up = 'data:image/gif;base64,R0lGODlhDwAPAJEAAP///9vg2kdSS////yH5BAEHAAMALAAAAAAPAA8AAAIknC2Zx6O/GJxnWgQDnQFoq3QiKIyj5YUAyTps9EYu2dANpjQFADs=';
if(close.tagName=='A')
{
close.removeAttribute('onclick');
close.setAttribute('href', 'javascript:;');
close.setAttribute('style', 'display:block; background:url('+icon_up+') !important;');
}
else
{
close.src = icon_up;
close.setAttribute('style', 'display:block; float:right;');
}
var box_toggle = function(){
if(box.hasAttribute('style'))
{
box.removeAttribute('style');
box.className = box.className.substr(0, box.className.length-20);
if(close.tagName!='A') close.src = icon_up;
else close.setAttribute('style', 'display:block; background:url('+icon_up+') !important;');
}
else
{
box.setAttribute('style', 'position:fixed; bottom:0; left:10px; right:10px; z-index:200;');
box.className += ' dt-floating-comment';
if(close.tagName!='A') close.src = icon_down;
else close.setAttribute('style', 'display:block; background:url('+icon_down+') !important;');
window.setTimeout(function(){body.focus();}, 100);
}
return false;
};
close.addEventListener('click', box_toggle, false);
close.title = 'Toggle floating comment box';
close.setAttribute('accesskey', 'r');
if(link)
{
link.href = "javascript:;";
link.removeAttribute('onclick');
link.addEventListener('click', box_toggle, false);
return 2;
}
else
{
return 1;
}
}
}
}
};
// Go!
devianttidy.start();
