There are 150 previous versions of this script.
Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)
// ==UserScript==
// @name Danbooru - Miscellaneous Tweaks
// @namespace http://userscripts.org/scripts/show/27776
// @description Adds a variety of useful tweaks to Danbooru, each of which can be easily disabled.
// @include http://danbooru.donmai.us/*
// @include https://danbooru.donmai.us/*
// @include http://hijiribe.donmai.us/*
// @include http://sonohara.donmai.us/*
// @include http://www.donmai.us/*
// @include http://donmai.us/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_log
// @grant GM_openInTab
// @version 2013.05.12
// ==/UserScript==
/*
Settings are modified by clicking the "Tweaks" link at the right end of the navbar on most pages.
You do not need to modify the script itself.
*/
var showError, recursion, login, loginID, content, navbar, subnavbar;
initialize();
loadTweaks();
function loadTweaks()
{
//Returns an array with just the "value" properties from an array of arguments for a tweak
function defaultArgs( tweak )
{
var defArgs = [];
for( var i = 0; i < tweak.args.length; i++ )
defArgs.push( tweak.args[i].value );
return defArgs;
}
//Validates the regex properties of an array of objects
function validRegexInArray(regexName){ return function(elem, args, argI){
var temp = args[argI], json = validJSON(elem, args, argI);
if( json !== false && (json instanceof Array) ) try {
for( var i = 0; i < json.length; i++ )
(new RegExp( "("+json[i][regexName]+")" )).test("blah");
return (args[argI] = json);
}
catch(err) { }
elem.style.color = "red";
args[argI] = temp;
return false;
}}
//Validates regex argument
function validRegex(elem, args, argI)
{
try{
var ex = new RegExp("("+elem.value+")");
ex.test("blah");
args[argI] = elem.value;
elem.style.removeProperty('color');
return elem.value;
}catch(err){
elem.style.color = 'red';
return false;
}
}
//Validates number argument
function validNumber(a,b,x)
{
var temp = function(elem, args, argI)
{
var num = Number( elem.value );
if( isNaN(num) || ( a !== undefined && num < a ) )
args[argI] = a;
else if( b > a && num > b )
args[argI] = b;
else
{
args[argI] = num;
elem.style.removeProperty('color');
return num;
}
elem.style.color = 'red';
return false;
}
if( x === undefined )
return temp;//Called with < 3 parameters. We just want the function
//Three params. We're setting a value without a range.
temp = validNumber();
return temp(a,b,x);
};
//Validates JSON argument
function validJSON(elem, args, argI)
{
try{
var value = MS_parseJSON( elem.value );
args[argI] = value;
elem.style.removeProperty("color");
return value;
}catch(err){}
elem.style.color = "red";
return false;
}
function validTagSubArray(elem, args, argI)
{
try{
var query = -1, call, json = MS_parseJSON( elem.value );
for( call = 0; call < json.length && query < 0 &&
json[call].title.length > 0 && // Non-empty title
json[call].refresh > 0 && // Refresh interval greater than zero
json[call].thumbs > 0 && // At least one thumbnail to display
json[call].queries.length > 0; call++ ) // At least one query
{
for( query = json[call].queries.length - 1; query >= 0 &&
json[call].queries[query].search.length > 0 && // Non-empty tags
json[call].queries[query].pages > 0 && // At least one page
json[call].queries[query].pages <= 1000 && // No more than 1000(!) pages
( !json[call].queries[query].filter || json[call].queries[query].filter instanceof Array ); query-- ); // Filter is an array if specified
}
if( query < 0 && call > 0 && call == json.length )
{
args[argI] = json;
elem.style.removeProperty("color");
return json;
}
}
catch(err){}
elem.style.color = "red";
return false;
}
function validSHA1(elem, args, argI)
{
if( /^[a-f0-9]{40}$/i.test( elem.value ) )
{
args[argI] = elem.value;
elem.style.removeProperty("color");
return elem.value;
}
elem.style.color = "red";
return false;
}
//Creates the input element for an element and sets up the appropriate settings/listeners
function setupArg( oldSettings, tweak, argI, elem )
{
//Let the starting value be the custom setting, or default if nothing set
var currentValue = tweak.args[argI].value;
if( oldSettings[ tweak.name ] && oldSettings[ tweak.name ].args && oldSettings[ tweak.name ].args[argI] != null )
currentValue = oldSettings[ tweak.name ].args[argI];
if( typeof( tweak.args[argI].value ) == "boolean" )
{
//Checkbox
elem = elem.appendChild( createElementX({ tag:"input", type:"checkbox", title:tweak.args[argI].desc }) );
elem.checked = currentValue;
tweak.args[argI].set = function(reset)
{
var settings = MS_getValue( "settings" );
if( !settings[ tweak.name ].args )
{
//Populate custom settings with the default arguments
settings[ tweak.name ].args = defaultArgs( tweak );
}
if( reset === true )
{
//Set to default value and delete the custom settings
elem.checked = tweak.args[argI].value;
settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
}
else settings[ tweak.name ].args[argI] = elem.checked;
MS_setValue( "settings", settings, -1 );
};
elem.addEventListener( "click", tweak.args[argI].set, false );
}
else if( typeof( tweak.args[argI].value ) == "number" )
{
if( !tweak.args[argI].setValid )
tweak.args[argI].setValid = validNumber;
elem = elem.appendChild( createElementX({ tag:"input", type:"text", title:tweak.args[argI].desc }) );
elem.value = currentValue;
tweak.args[argI].set = function(reset)
{
var value, settings = MS_getValue( "settings" );
if( !settings[ tweak.name ].args )
settings[ tweak.name ].args = defaultArgs( tweak );
if( reset === true )
{
settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
elem.value = tweak.args[argI].value;
elem.style.removeProperty("color");//Assume default arguments are valid
}
else tweak.args[argI].setValid( elem, settings[ tweak.name ].args, argI );
MS_setValue( "settings", settings, -1 );
};
elem.addEventListener( "input", tweak.args[argI].set, false );
}
else if( typeof( tweak.args[argI].value ) == "string" )
{
if( tweak.args[argI].elem == "input" )
{
elem = elem.appendChild( createElementX({ tag:"input", type:"text", title:tweak.args[argI].desc }) );
//Size element to fit the contents
elem.addEventListener( "input", function() { elem.setAttribute("size", elem.value.length + 5); }, false );
elem.setAttribute("size", currentValue.length + 5);
}
else
{
//Size to fit, with a timeout since hidden elements don't have a height
elem = elem.appendChild( createElementX({ tag:"textarea", type:"text", style:"display: inline-block; width:95%; height:20px", title:tweak.args[argI].desc }) );
setTimeout( function() { elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight, 20 ) + "px"; }, 0 );
elem.addEventListener( "input", function() { elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight ) + "px"; }, false );
}
elem.value = currentValue;
tweak.args[argI].set = function(reset)
{
var settings = MS_getValue( "settings" );
if( !settings[ tweak.name ].args )
settings[ tweak.name ].args = defaultArgs( tweak );
if( reset === true )
{
settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
elem.value = tweak.args[argI].value;
elem.style.removeProperty("color");
}
else if( tweak.args[argI].setValid )
tweak.args[argI].setValid( elem, settings[ tweak.name ].args, argI );
else
settings[ tweak.name ].args[argI] = elem.value;//No validation function, assume valid
MS_setValue( "settings", settings, -1 );
}
elem.addEventListener( "input", tweak.args[argI].set, false );
}
else if( typeof( tweak.args[argI].value ) == "object" )
{
if( !tweak.args[argI].setValid )
tweak.args[argI].setValid = validJSON;
if( tweak.args[argI].desc.length )
elem.appendChild( createElementX({tag:"br"}) );
elem = elem.appendChild( createElementX({ tag:"textarea", type:"text", style:"display: inline-block; width:95%; height:20px", title:tweak.args[argI].desc }) );
setTimeout( function() { elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight, 20 ) + "px"; }, 0 );
//Insert spaces to help with linebreaking
elem.value = JSON.stringify( currentValue, null, 1 ).replace(/\s+/g,' ');
tweak.args[argI].set = function(reset)
{
var settings = MS_getValue( "settings" );
if( !settings[ tweak.name ].args )
settings[ tweak.name ].args = defaultArgs( tweak );
if( reset === true )
{
settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
elem.value = JSON.stringify( tweak.args[argI].value, null, 1 ).replace(/\s+/g,' ');
elem.style.removeProperty("color");
}
else tweak.args[argI].setValid( elem, settings[ tweak.name ].args, argI );
elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight) + "px";
MS_setValue( "settings", settings, -1 );
}
elem.addEventListener( "input", tweak.args[argI].set, false );
}
else tweak.args[argI].set = function(){ GM_log("No setter for type "+typeof( tweak.args[argI].value )+" ("+tweak.name+")"); }
}
var defaultAddStyle =
'/* Hide the Danbooru header */\n'+
'header h1 { display: none }\n'+
'\n'+
'/* Shove the forum post IDs listed on the Reply rows to the right and color them white. */\n'+
'div.list-of-forum-posts menu li:first-child { float:right; color:white }\n'+
'\n'+
'/* Hide the post search Go button */\n'+
'*#search-box form input[type="submit"] { display: none }\n'+
'\n'+
'/* Increase the space above the navbar */\n'+
'menu.main { margin-top: 15px }\n'+
'\n'+
'/* Reduce height between thumbnail blocks on profile pages */\n'+
'div#c-users div#a-show div.box { margin-bottom: 0em; }\n'+
'\n'+
'/* Make font size more uniform */\n'+
'body { font-size: 80% }\n'+
'textarea { font-size: 1.2em }\n'+
'\n'+
'/* Increase the space around thumbnails */\n'+
'article.post-preview { min-height: 175px; min-width: 165px; }\n'+
'.preview { min-height: 175px; min-width: 160px; }\n'+
'\n'+
'/* Put a little space between the thumbnails and their status borders. */\n'+
'.post-preview img { padding:2px }\n'+
'\n'+
'/* Shrink the comment width a tiny bit to accommodate the above width changes */\n'+
'.list-of-comments { width: 98% }\n'+
'\n'+
'/* Increase space between the Popular navigators */\n'+
'div#c-explore-posts .period { margin: 0px 5em; }\n'+
'\n'+
'/* Reduce padding around post status notices */\n'+
'div#c-posts div#a-show #pool-nav, div#c-posts div#a-show #search-seq-nav, div#c-posts div#a-show #nav-help { margin: 0px 0px 0.5em 0px; }\n'+
'div#c-posts div.nav-notice { padding: 0.5em; }\n'+
'div#c-posts div.notice { padding: 0.5em; margin-bottom: 0.5em; }\n'+
'\n'+
'/* Hide upload guide notice */\n'+
'div#upload-guide-notice { display:none }\n';
var tweaks =
[
{ name:"Metasettings", enable:false, desc:"Settings affecting the script's operation as a whole.", func:metaSettings, version:2, args:[{ desc:"Check for script updates once a day (requires GM_xmlhttpRequest)", value:true }, { desc:"Display alerts if tweaks fail", value:false } ] },
{ name:"Add Tag Subscription", enable:false, desc:"Adds pseudo tag subscriptions to your user page.<br><u>title</u>: Title of the subscription, linked to the first query. The title must be unique.<br><u>timestamp</u>: Appends a timestamp of the last refresh to the title.<br><u>refresh</u>: Hours to wait before refreshing the subscription.<br><u>thumbs</u>: Number of thumbnails to display.<br><u>intersect</u>: Return posts matching ALL queries (using the order of the last) rather than ANY (sorted by post ID).<br><u>queries</u>: List of objects representing post searches.<br><u>search</u>: Tags to search for; the usual limitations apply.<br><u>pages</u>: Number of pages to collect for this query, each page containing 100 posts.<br><u>filter</u>: Drop any posts from this query's results that don't also contain all of the tags in the filter. Metatags are not supported.", func:addTagSubscription, version:1, args:[ { desc:"", value:[ { title:"Recently Translated", timestamp:false, refresh:1, thumbs:6, intersect:true, queries:[ { search:"order:note user:"+login, pages:1, filter:[] } ] }, { title:"Recently Commented", timestamp:false, refresh:1, thumbs:6, intersect:true, queries:[ { search:"order:comment user:"+login, pages:1, filter:[] } ] } ], setValid:validTagSubArray } ] },
{ name:"Arrow Key Navigation", enable:false, desc:"Allows navigation between pages using the arrow keys.", func:arrowKeyNavigation, version:0, args:[] },
{ name:"Blacklist", enable:false, desc:"Blacklist implementation that replaces images (full and thumbnail) with links labeled with the tags they matched in the blacklist. Clicking the link/image alternates between the two. Translation notes appear every other time the image is displayed. User and rating metatags are supported. Your own posts are exempt from the blacklist.", func:blacklistTags, version:0, args:[{ desc:"Input to regex constructor [/^(xxx)$/i]", value:"blockedTag1|blockedTag2|blockedTag3", setValid:validRegex }] },
{ name:"Change +/- Links", enable:false, desc:"Adds +/- tag links when the taglist doesn't have them.", func:changePlusMinusLinks, version:1, args:[{ desc:"Make links add/remove tags from search box instead of launching new searches", value:true }] },
{ name:"Custom Taglist", enable:false, desc:"Adds a new taglist section below tag lists, complete with +/- links. Tag types are supported.", func:customTaglist, version:0, args:[{ desc:"Title of list", value:"Custom Tags", elem:"input" }, { desc:"Tags", value:[ 'art:bomber_grape', 'char:maribel_hearn', 'copy:hidamari_sketch', 'order:score', 'order:comment', 'comm:'+login, 'rating:e', 'rating:q', 'rating:s', 'status:any', 'fav:'+login, 'user:'+login, 'source:pixiv/*', 'arttags:0', 'chartags:1', 'copytags:1', 'gentags:1' ] }] },
{ name:"Double Down", enable:false, desc:"Requires down-vote links on posts and comments to be clicked twice in a row to count.", func:doubleDown, version:0, args:[ { desc:"Color to apply to down links the first time they are clicked", value:"red", elem:"input" } ] },
{ name:"Hide User Statistics", enable:false, desc:"", func:hideUserStatistics, version:0, args:[{ desc:"Stats to remove from your profile", value:[ "Statistics", "Inviter", "Approvals" ] }, { desc:"Stats to remove from others' profiles", value:[ "Inviter" ] }] },
{ name:"Navbar Links", enable:false, desc:"Adds links to the left side of the navbar.", func:navbarLinks, version:0, args:[{ desc:"", value:[{ text:"Upload", href:"/uploads/new" }, { text:"IQDB", href:"http://danbooru.iqdb.org/" }] }] },
{ name:"Navbar Tag Search", enable:false, desc:"Adds/moves the post search box to the navbar.", func:navbarTagSearch, version:1, args:[{ desc:"Move the post search box when it exists", value:true }, { desc:"Add to subnavbar instead of navbar", value:true }] },
{ name:"Precision Time", enable:false, desc:"Sets all time fields to the 'X units ago' form.", func:precisionTime, version:0, args:[ { desc:"Precision", value:1, setValid:validNumber(0,6) } ] },
{ name:"Score Thumbnails", enable:false, desc:"Displays scores and favorite counts below thumbnails.", func:scoreThumbnails, version:0, args:[ { desc:"Show score", value:true }, { desc:"Minutes to keep favcount in cache (0 = no favcount)", value:0, setValid:validNumber(0) } ] },
{ name:"Source Stat Links", enable:false, desc:"Running artist/source searches for posts without artist tags and displays the results under the source information in the sidebar.", func:sourceStatLinks, version:0, args:[{ desc:"Run \"Find Artist\" search", value:true }, { desc:"Run post search", value:false }] },
{ name:"Style User Levels", enable:false, desc:"Applies styles to links to user pages depending on their level.", func:styleUserLevels, version:1, args:[ { desc:"Days to cache each user level", value:5, setValid:validNumber(1,90) }, { desc:"Styles", value:{ Admin: 'color:red', Mod: 'color:orange', Janitor: 'color:green', Contributor: 'color:purple', Builder: 'color:#6633FF', Platinum: 'color:#0000FF', Gold: 'color:#0000FF', Member: '', Blocked: 'color:black', Unknown:"color:gray"} } ] },
{ name:"Add Style", enable:false, desc:"Adds CSS styles to all pages. The 'important' flag is added automatically. NOTE: This tweak is the most likely to reset after script updates, so backup any changes you make.", func:MS_addStyle, version:6, args:[{ desc:"", value:defaultAddStyle }] }
];
var i, settings = MS_getValue( "settings", {} ), changed = false;
for( i = 0; i < tweaks.length; i++ )
{
if( !settings[ tweaks[i].name ] || settings[ tweaks[i].name ].version != tweaks[i].version )
{
settings[ tweaks[i].name ] = { enable:tweaks[i].enable, version:tweaks[i].version };
changed = true;
}
try{
if( settings[ tweaks[i].name ].args )
{
//Custom settings exist. Run tweak if enabled
if( settings[ tweaks[i].name ].enable )
tweaks[i].func.apply( this, settings[ tweaks[i].name ].args );
}
else if( tweaks[i].enable )
{
//No custom settings, run with default arguments if enabled by default
tweaks[i].func.apply( this, defaultArgs( tweaks[i] ) );
}
}
catch(e) { if( showError ) MS_alert("Error ("+tweaks[i].name+"): "+e); }
}
if( changed )
MS_setValue( "settings", settings, -1 );
content = !recursion && navbar && document.getElementById("page");
if( content )
{
var settingsLink = navbar.appendChild( createElementX({ tag: "li" }) ).appendChild( createElementX({ tag: "a", text: "Tweaks" }) );
var settingsDiv = content.parentNode.insertBefore( createElementX({ tag:"div", style:"display:none; padding:0 20px 30px 20px;", id:"tweaks_settings" }), content );
settingsLink.addEventListener( "mousedown", function()
{
if( content.style.display == "none" )
{
//Closing settings. Destroy the contents and unhide the original content.
settingsDiv.style.display = "none";
content.style.display = "block";
settingsDiv.textContent = "";
}
else
{
//Opening settings. Hide the current content and reconstruct the GUI from scratch.
var settings = MS_getValue( "settings", {} );
settingsDiv.appendChild( createElementX({ tag:"h4" }) ).appendChild( createElementX({ tag:"a", href:"http://userscripts.org/scripts/show/27776", text:"Miscellaneous Tweaks", style:"color:black;" }, " settings") );
settingsDiv.appendChild( createElementX( { tag:"hr" }, "Questions? Comments? Problems? Leave some feedback at ", { tag:"a", href:"http://userscripts.org/scripts/discuss/27776", text:"http://userscripts.org/scripts/discuss/27776" }, "!", { tag:"hr" }) );
var table = settingsDiv.appendChild( createElementX({ tag:"table", width:"100%", style:"margin-bottom: 2em; vertical-align:top;" }) );
var resetAll = false, resetButtons = [];
//Clear old unused settings
for( sName in settings )
{
for( var i = 0; i < tweaks.length && sName != tweaks[i].name; i++ );
if( i == tweaks.length )
{
GM_log("Deleting old setting: "+sName);
delete settings[sName];
MS_setValue( "settings", settings, -1 );
}
}
for( var i = 0; i < tweaks.length; i++ )
{
var reset, body, check, title, tr = table.appendChild( createElementX({ tag:"tr", style:"vertical-align:top;" }) );
var border = i != tweaks.length - 1 ? "border-bottom:4px solid #EEE; " : "";
//Button to reset to default settings
reset = tr.appendChild( createElementX({ tag:"td", style:border+"padding:1px 4px 1em 4px; width:1%" }) ).appendChild( createElementX({ tag:"button", title:"Reset to default settings for this tweak.", text:"Reset" }) );
resetButtons.push( reset );//pad 1 4
//Checkbox to toggle tweak
check = tr.appendChild( createElementX({ tag:"td", style:border+"padding:0.3em 4px 1em 4px; text-align:center" }) ).appendChild( createElementX({ tag:"input", type:"checkbox", title:"Toggle this tweak." }) );//pad 6 4
check.checked = ( settings[ tweaks[i].name ].args ? !!settings[ tweaks[i].name ].enable : !!tweaks[i].enable );
//Tweak description and arguments
body = tr.appendChild( createElementX({ tag:"td", style:border+"padding:1px 4px 1em 4px; " }) );//pad 1 4 10 4
title = body.appendChild( createElementX({ tag:"b", text:tweaks[i].name }) );
title.style.setProperty( "text-decoration", check.checked ? "none" : "line-through", null );
//body.appendChild( createElementX(": "+tweaks[i].desc) );
body.appendChild( createElementX(": ") );
body.appendChild( createElementX({ tag:"span" }) ).innerHTML = tweaks[i].desc;
//When the Reset button is clicked, delete any custom settings for it and fall back on defaults.
reset.addEventListener( "click", (function(tweak,check){ return function()
{
if( !resetAll && !confirm("Reset this tweak?") )
return;
if( !!check.checked != !!tweak.enable )
check.click();
for( var i = 0; i < tweak.args.length; i++ )
tweak.args[i].set(true);
}; })( tweaks[i], check ), false );
//When checkbox is clicked, toggle the title strikethrough and save settings
check.addEventListener( "click", (function(tweak,title){ return function()
{
var settings = MS_getValue( "settings" );
title.style.setProperty( "text-decoration", (settings[ tweak.name ].enable = this.checked) ? "none" : "line-through", null );
if( !settings[tweak.name].args )
settings[tweak.name].args = defaultArgs( tweak );
MS_setValue( "settings", settings, -1 );
}; })(tweaks[i],title), false );
//If the tweak takes arguments, create the appropriate input elements and listeners.
if( tweaks[i].args.length )
{
var list = body.appendChild( createElementX({ tag:"ul", style:"margin:0 0 0 2em; list-style:disc" }) );
for( var j = 0; j < tweaks[i].args.length; j++ )
setupArg( settings, tweaks[i], j, list.appendChild( createElementX({ tag:"li", text:tweaks[i].args[j].desc+(tweaks[i].args[j].desc.length ? ": " : ""), style:"margin-top:4px" }) ) );
}
}
settingsDiv.appendChild( createElementX({ tag:"hr" }) );
//"Export" button
settingsDiv.appendChild( createElementX({ tag:"input", type:"button", value:"Export", title:"Save settings to file. There is no import option." }) ).addEventListener( "click", function()
{
try{ window.location ="data:application/octet-stream,"+escape( JSON.stringify( MS_getValue( "settings" ), null, 1 ).replace(/\s+/g,' ') ); }
catch(err){}
}, false );
//"Reset All Tweaks" button
var resetAllButton = settingsDiv.appendChild( createElementX({ tag:"input", type:"button", value:"Reset All Tweaks", title:"Reset all settings." }) ).addEventListener( "click", function()
{
if( confirm("Reset all tweaks?") )
{
resetAll = true;
for( var i = 0; i < resetButtons.length; i++ )
resetButtons[i].click();
resetAll = false;
}
}, false );
//"Delete All Variables" button
var resetAllButton = settingsDiv.appendChild( createElementX({ tag:"input", type:"button", value:"Delete All Variables", title:"Reset all settings." }) ).addEventListener( "click", function()
{
if( confirm("WARNING: This will delete all variables associated with this script.") && confirm("...Really?") )
{
var varList = GM_listValues();
while( varList.length > 0 )
GM_deleteValue( varList.pop() );
settingsLink.click();
}
}, false );
content.style.display = "none";
settingsDiv.style.display = "block";
}
}, false );
}
}
function initialize()
{
if( typeof(unsafeWindow) == "undefined" )
var unsafeWindow=window;
if( typeof(GM_getValue) == "undefined" || GM_getValue('a', 'b') == undefined )
{
GM_log = console.log || function() { };
//http://userscripts.org/topics/41177
GM_deleteValue = function(a){ localStorage.removeItem("danbooru_miscellaneous."+a); }
GM_openInTab = function(a){ return window.open(a,a); }
GM_getValue = function(name, defaultValue)
{
var value = localStorage.getItem("danbooru_miscellaneous."+name);
if( !value )
return defaultValue;
var type = value[0];
value = value.substring(1);
if( type == 'b' )
return value == 'true';
else if( type == 'n' )
return Number(value);
return value;
}
GM_setValue = function(name, value)
{
value = (typeof value)[0] + value;
localStorage.setItem("danbooru_miscellaneous."+name, value);
}
GM_listValues = function()
{
var i, j = 0, list = new Array(localStorage.length);
for( i = 0; i < localStorage.length; i++ )
if( /^danbooru_miscellaneous/.test( localStorage.key(i) ) )
list[j++] = localStorage.key(i).replace(/^danbooru_miscellaneous./,'');
return list;
}
GM_xmlhttpRequest = function(obj)
{
//Unlike the Greasemonkey function, XMLHttpRequest can't access sites outside Danbooru
if( !/^https?:..[^.]+.donmai.us\//.test(obj.url) )
return;
var request = new XMLHttpRequest();
request.onreadystatechange = function()
{
if( obj.onreadystatechange )
obj.onreadystatechange( request );
if( request.readyState == 4 && obj.onload )
obj.onload( request );
}
request.onerror = function()
{
if( obj.onerror )
obj.onerror( request );
}
try {
request.open( obj.method, obj.url, true );
} catch(e) {
if( obj.onerror )
obj.onerror( { readyState:4, responseHeaders:'', responseText:'', responseXML:'', status:403, statusText:'Forbidden'} );
return;
}
if( obj.headers )
for( name in obj.headers )
request.setRequestHeader( name, obj.headers[name] );
request.send( obj.data );
return request;
}
}
showError = false;
recursion = (window != window.top);
loginID = document.getElementsByName("current-user-id");
if( loginID.length == 0 )
{
login = "";
loginID = 0;
}
else
{
login = document.getElementsByName("current-user-name")[0].content;
loginID = parseInt(loginID[0].content);
}
//pass_hash = document.cookie.search("cookie_password_hash=(.+?)(;|$)") < 0 ? "" : document.cookie.match("cookie_password_hash=(.+?)(;|$)")[1];
navbar = document.getElementById("site-map-link");
if( navbar )
{
content = !recursion && document.getElementById("page");
navbar = navbar.parentNode.parentNode;
for( subnavbar = navbar.nextSibling; subnavbar && !subnavbar.tagName; subnavbar = subnavbar.nextSibling );
if( !subnavbar || subnavbar.tagName != "MENU" )
{
//Insert subnavbar if it doesn't exist but the navbar does.
subnavbar = navbar.parentNode.insertBefore( createElementX({tag:"menu"}), navbar.nextSibling );
//Add a dummy link element so the subnavbar is the right height.
subnavbar.appendChild( createElementX({tag:"li"}) ).appendChild( createElementX({tag:"a", href:"#"}) );
}
}
if( typeof(custom) != "undefined" )
custom();
}
function metaSettings(aUpdateCheck, aShowError)
{
if( aUpdateCheck )
updateCheck();
showError = aShowError;
}
/**
Checks for script updates.
*/
function updateCheck()
{
var scriptNum = 27776;
//Only check for update if using Greasemonkey and no check has been made in the last day.
if( !MS_getValue('checked_update') )
{
MS_setValue( 'checked_update', true, 24*3600 );
GM_xmlhttpRequest(
{
method: 'GET',
url: 'http://userscripts.org/scripts/source/'+scriptNum+'.meta.js?'+new Date().getTime(),
headers: { 'Cache-Control': 'no-cache' },
onload: function(response)
{
var localVersion = parseInt( GM_getValue( 'local_version', 0 ) );
var remoteVersion = parseInt( /@uso:version\s*([0-9]+?)\s*$/m.exec(response.responseText)[1] );
if( !localVersion || remoteVersion <= localVersion )
GM_setValue( 'local_version', remoteVersion );
else if( confirm( 'There is an update available for the Greasemonkey script "'+/@name\s*(.*?)\s*$/m.exec(response.responseText)[1]+'".\nWould you like to go to the install page now?' ) )
{
GM_openInTab( 'http://userscripts.org/scripts/show/'+scriptNum );
GM_setValue( 'local_version', remoteVersion );
}
},
});
}
}
/**
Adds/moves the post search box to the navbar.
*/
function navbarTagSearch(moveBox,useSubnavbar)
{
var targetBar = useSubnavbar ? subnavbar : navbar;
if( recursion || !targetBar )
return;
var searchBox = document.getElementById("search-box"), value = "";
if( searchBox )
{
if( !moveBox )
return;
if( document.getElementById("tags") )
value = document.getElementById("tags").value.replace(/"/g,'');
//To keep a consistent location for the search box, remove the original one wherever it appears.
searchBox.parentNode.removeChild(searchBox);
}
searchBox = createElementX({ tag:"li", id:"search-box"});
searchBox.innerHTML = '<form accept-charset="UTF-8" action="/posts" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="✓" /></div><input id="tags" name="tags" size="30" type="text" ' + ( value ? 'value="'+value+'" ' : 'placeholder="Search posts" ' ) + '/><input name="commit" type="submit" value="Go" style="display:none" /></form>';
targetBar.insertBefore( searchBox, targetBar.firstChild );
}
/**
Causes the +/- links next to tags in the taglist (which appear for normal tags for Privileged+ users)
to add/remove/negate tags from the tag search box. Tags cannot be added or negated more than once.
*/
function changePlusMinusLinks(addListeners)
{
if( recursion || !document.getElementById("tags") )
return;
setTimeout( function() //0-timeout to let the custom tags get added
{
var boxValue = document.getElementById("tags").value;
if( boxValue.length > 0 )
boxValue = '+'+boxValue;
var tagList = document.evaluate( "//section/ul/li/a[@class='wiki-link' and text() = '?']/..", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
for( var i = 0; i < tagList.snapshotLength; i++ )
{
var links = tagList.snapshotItem(i).getElementsByTagName("a");
if( links.length < 4 )
{
var urlTag = links[0].href.replace( /.*title=/i, '' );
tagList.snapshotItem(i).insertBefore( createElementX(
" ", { tag:'a', text:'+', class:"search-inc-tag", href:"/posts?tags=" +urlTag+boxValue },
" ", { tag:'a', text:'-', class:"search-exl-tag", href:"/posts?tags=-"+urlTag+boxValue }), links[0].nextSibling );
links = tagList.snapshotItem(i).getElementsByTagName("a");
links[2].innerHTML="–";
}
if( addListeners )
{
var tag = links[3].textContent.replace( /\s+/g, '_' );
//Plus
links[1].setAttribute("onclick","return false;");
links[1].addEventListener( "click", searchBoxChange(" "+tag+" ", " -"+tag+" "), false );
//Minus
links[2].setAttribute("onclick","return false;");
links[2].addEventListener( "click", searchBoxChange(" -"+tag+" ", " "+tag+" "), false );
}
}
}, 0 );
function searchBoxChange(add, remove) { return function()
{
var searchBox = document.getElementById("tags");
var value = " "+searchBox.value+" ";
if( value.indexOf(remove) >= 0 )
value = value.replace(remove," ");
else if( value.indexOf(add) < 0 )
value = add + value;
//Trim excess whitespace from search box and give it focus
searchBox.value = value.replace(/(^ +| +$)/g,'').replace(/ +/,' ');
searchBox.focus();
}}
}
/**
Adds a new section to the taglist, complete with +/- links.
*/
function customTaglist(title, newTags)
{
var tagList = !recursion && document.evaluate( "//section/ul/li/a[@class='wiki-link' and text() = '?']/../../..", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue;
if( !tagList )
return;
if( !newTags || !(newTags instanceof Array ) || !newTags.length )
throw "Parameter must be a non-empty array."
var fragment = createElementX([{ tag:"h1", text:title }]);
var newList = fragment.appendChild( createElementX({tag:"ul"}) );
var tagsValue = document.getElementById("tags").value;
if( tagsValue.length > 0 )
tagsValue = '+'+tagsValue;
for( var i = 0; i < newTags.length; i++ )
{
var newListItem = createElementX({ tag: "li", class:"category-0"/* general tag by default */ });
var thisTag = newTags[i];//Make copy to avoid removing tag type from original settings
if( thisTag.indexOf("art:") == 0 )
newListItem.className = "category-1";
else if( thisTag.indexOf("char:") == 0 )
newListItem.className = "category-4";
else if( thisTag.indexOf("copy:") == 0 )
newListItem.className = "category-3";
thisTag = thisTag.replace(/^(art|char|copy|gen):/,'');
if( thisTag.indexOf(":") > 0 )
newListItem.innerHTML = '<a class="wiki-link" href="/wiki_pages?title=help:cheatsheet">?</a>';
else
newListItem.innerHTML = '<a class="wiki-link" href="/wiki_pages?title='+thisTag.replace(/ /g,'_')+'">?</a>';
newListItem.innerHTML += ' <a href="/posts?tags='+thisTag.replace(/ /g,'_')+tagsValue+'" class="search-inc-tag">+</a> <a href="/posts?tags=-'+thisTag.replace(/ /g,'_')+tagsValue+'" class="search-exl-tag">–</a>'+' <a href="/posts?tags='+thisTag.replace(/ /g,'_')+'" class="search-tag">'+thisTag.replace(/_/g,' ')+'</a>';
newList.appendChild(newListItem);
}
tagList.appendChild(fragment);
}
/**
Adds links to the navbar.
*/
function navbarLinks(links)
{
if( !navbar || recursion || !links || !links.length )
return;
var fragment = createElementX([]);
for( var i = 0; i < links.length; i++ )
if( links[i].text && links[i].href )
fragment.appendChild( createElementX({ tag: "li" }) )
.appendChild( createElementX({ tag:"a", href:links[i].href, text:links[i].text }) );
navbar.insertBefore( fragment, navbar.firstChild );
}
/**
Removes the specified statistics from user profiles.
*/
function hideUserStatistics(deleteMe, deleteThem)
{
var deleteArray, statBlock = !recursion && document.getElementsByClassName("user-statistics")[0];
if( !statBlock )
return;
if( statBlock.parentNode.parentNode.getElementsByTagName("h1")[0].textContent == login )
deleteArray = ( deleteMe instanceof Array ) ? deleteMe : [];//Your profile
else
deleteArray = ( deleteThem instanceof Array ) ? deleteThem : [];//Someone else's profile
if( deleteArray.indexOf("Statistics") >= 0 )
statBlock.parentNode.getElementsByTagName("h2")[0].style.display = "none";
var statLabels = statBlock.getElementsByTagName("th");
for( var i = 0; i < statLabels.length; i++ )
if( deleteArray.indexOf( statLabels[i].textContent ) >= 0 )
statLabels[i].parentNode.style.display = "none";
}
/**
Applies style attributes to links to user pages depending on the type of user.
refreshDays: Number of days to cache a specific user's level
styleObj: Collection of styles applied to the different user levels
User level info @ danbooru/app/models/user.rb
*/
function styleUserLevels( refreshDays, styleObj )
{
if( recursion )
return;
var userCache, loadAgain = true;
MS_observeInserts( subStyle )();
function subStyle(e)
{
if( e && !e.target.getElementsByTagName )
return;
var userLinks = new XPathEvaluator().evaluate('descendant-or-self::a[contains(@href,"/users/")]', (e ? e.target : document), null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
if( userLinks.snapshotLength > 0 ) setTimeout( function()
{
var missing = {};
if( loadAgain )
{
loadAgain = false;
setTimeout( function(){ loadAgain = true; }, 2000 );
userCache = MS_getValue( "styleUserLevels.cache" ) || { saveName: "styleUserLevels.cache", expires: refreshDays*24*3600 };
}
for( var i = 0; i < userLinks.snapshotLength; i++ )
{
var link = userLinks.snapshotItem(i);
if( /^https?:..[^\.]+.donmai.us.users.\d+/.test( link.href ) )
{
var index = link.href.replace( /.*users\//, 'id=' );
var info = 0;
if( link.textContent.indexOf("user #") < 0 && link.className )
{
if( link.className.indexOf("user-banned") >= 0 )
info = { level:10 };
else if( link.className.indexOf("user-member") >= 0 )
info = { level:20 };
else if( link.className.indexOf("user-gold") >= 0 )
info = { level:30 };
else if( link.className.indexOf("user-platinum") >= 0 )
info = { level:31 };
else if( link.className.indexOf("user-builder") >= 0 )
info = { level:32 };
else if( link.className.indexOf("user-contributor") >= 0 )
info = { level:33 };
else if( link.className.indexOf("user-janitor") >= 0 )
info = { level:35 };
else if( link.className.indexOf("user-moderator") >= 0 )
info = { level:40 };
else if( link.className.indexOf("user-admin") >= 0 )
info = { level:50 };
}
if( !info )
info = MS_getValue( index, userCache );
if( info )
setUserStyle( link, info );
else if( !missing[index] )
missing[index] = [ link ];
else
missing[index].push( link );
}
}
for( elem in missing )
getLevel( elem, missing[elem] );
}, 1 );
}
function getLevel( index, linkArray )
{
MS_requestAPI( '/user/index.json?'+index, function(responseDetails)
{
var result;
try{ result = MS_parseJSON(responseDetails.responseText); }
catch(e) { getLevel( index, linkArray ); return; }
for( var i = 0; i < result.length; i++ )
MS_setValue( 'id='+result[i].id, { name:result[i].name, level:result[i].level }, refreshDays*24*3600, userCache );
var info = MS_getValue( index, userCache ) || { level:20 };
if( !info.name )
MS_setValue( index, info, refreshDays*24*3600, userCache );//default level is Member
for( var i = 0; i < linkArray.length; i++ )
setUserStyle( linkArray[i], info );
});
}
function setUserStyle( link, info )
{
var style, className = "";
switch( info.level )
{
case 50: style = styleObj.Admin; className = "user-admin"; break;
case 40: style = styleObj.Mod; className = "user-moderator"; break;
case 35: style = styleObj.Janitor; className = "user-janitor"; break;
//case 34: style = styleObj.TestJanitor; break;//"TestJanitor": "color:#4CC417",
case 33: style = styleObj.Contributor; className = "user-contributor"; break;
case 32: style = styleObj.Builder; className = "user-builder"; break;
case 31: style = styleObj.Platinum; className = "user-platinum"; break;
case 30: style = styleObj.Gold; className = "user-gold"; break;
case 20: style = styleObj.Member; className = "user-member"; break;
case 10: style = styleObj.Blocked; className = "user-banned"; break;
default: style = styleObj.Unknown; break;//unknown power level
}
if( style && style.length > 0 )
{
if( /^color:[^;]+$/.test(style) )
link.style.color = style.replace('color:','');
else
link.setAttribute( "style", link.getAttribute("style", "")+"; "+style );
}
if( className && link.className.indexOf(className) < 0 )
link.className += (link.className ? ' ' : '')+className;
if( info.name && /^user #\d+$/.test(link.textContent) )
link.textContent = info.name;
}
}
/**
Adds back navigation by arrow keys.
*/
function arrowKeyNavigation(useArrows)
{
if( recursion )
return;
var leftLink = 0, rightLink = 0;
var links = document.getElementById("popular-nav-links") ? document.getElementById("popular-nav-links").getElementsByTagName("a") : [];
if( links.length == 9 )
{
//Popular
if( location.search.indexOf("scale=month") >= 0 )
{
leftLink = links[6];
rightLink = links[8];
}
else if( location.search.indexOf("scale=week") >= 0 )
{
leftLink = links[3];
rightLink = links[5];
}
else
{
leftLink = links[0];
rightLink = links[2];
}
}
else if( (links = document.getElementsByClassName("paginator")[0]) != null && (links = links.getElementsByTagName("li")).length > 0 )
{
//Paginator. Link will be a <span> element if there are no more pages in the requested direction.
leftLink = links[0].firstChild;
rightLink = links[links.length - 1].firstChild;
}
else if( (links = document.getElementById("nav-links")) != null && (links = links.getElementsByTagName("li")[0]) != null )
{
leftLink = links.getElementsByClassName("prev")[0];
rightLink = links.getElementsByClassName("next")[0];
}
else return;
//Listen for left/right arrow key presses
window.addEventListener( "keydown", function(key)
{
//Don't navigate if Alt/Ctrl/Shift are also being pressed, or if a text field has focus.
if( key.altKey || key.ctrlKey || key.shiftKey || key.metaKey || /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName) )
return;
if( key.keyCode == 37 && leftLink && leftLink.href )
leftLink.click();
else if( key.keyCode == 39 && rightLink && rightLink.href )
rightLink.click();
}, false );
}
/**
Appends images' scores and/or favorite counts below their thumbnails.
appendScore: If true, append the score.
appendFavcount: If nonzero, append the favcount. This specifies the minutes to keep favcount in cache.
*/
function scoreThumbnails( appendScore, appendFavcount )
{
if( recursion || !content )
return;
if( appendFavcount > 0 )
appendFavcount *= 60;
else
appendFavcount = 0;
setTimeout( function() { MS_observeInserts( giveSomething )(); }, 0 );
var favcountCache;
function giveSomething(e)
{
if( e && !e.target.getElementsByTagName )
return;
var thumbList = new XPathEvaluator().evaluate("descendant-or-self::node()[contains(@class,'post-preview')]", (e ? e.target : document), null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var favIDs = "", divList = [];
if( !thumbList.snapshotLength )
return;
if( appendFavcount )
{
favcountCache = MS_getValue( "giveThumbnailsScores.favCount" ) || { saveName: "giveThumbnailsScores.favCount", expires: appendFavcount*2 };
}
for( var i = 0; i < thumbList.snapshotLength; i++ )
{
if( thumbList.snapshotItem(i).getElementsByClassName("misc_thumb_scores")[0] )
continue;
/* Comment index has a different structure than thumbs elsewhere. */
var imageBox = thumbList.snapshotItem(i).getElementsByTagName("img")[0].parentNode;
var postID = thumbList.snapshotItem(i).getAttribute("data-id") || imageBox.href.replace(/.*\//,'');
imageBox = imageBox.parentNode;
/* The default height for thumbnails will cause most scores to be hidden, so
try to override the height here in case the user hasn't already done so. */
imageBox.style.minHeight = "170px";
var newDiv = imageBox.appendChild( createElementX({ tag: "div", class: "misc_thumb_scores", style:"text-align:center" }) );
if( appendScore )
{
var score = thumbList.snapshotItem(i).getAttribute("data-score",0);
newDiv.appendChild( createElementX("Score: "+score) );
if( score < 0 )
newDiv.style.color = "red";
}
if( appendFavcount )
{
var favcount = thumbList.snapshotItem(i).getAttribute("data-favcount");//Attribute added by this script.
if( favcount != null )
MS_setValue( thumbList.snapshotItem(i).id, favcount, appendFavcount, favcountCache );
else
favcount = MS_getValue( thumbList.snapshotItem(i).id, favcountCache );
if( favcount != null )
newDiv.appendChild( createElementX( " ("+favcount+" fav"+( favcount == 1 ? ")" : "s)" ) ) );
else
{
divList.push({ "id":postID, "div":newDiv });
favIDs += ( favIDs.length > 0 ? "," : "" ) + postID;
}
}
}/* loop over thumbs */
MS_requestAPI( "/posts.json?tags=status:any+id:"+favIDs, function(responseDetails)
{
var result;
try{ result = MS_parseJSON(responseDetails.responseText); }
catch(e){ return; }
for( var i = 0; i < result.length; i++ )
{
MS_setValue( "post_"+result[i].id, result[i].fav_count, appendFavcount, favcountCache );
for( var j = 0; j < divList.length; j++ )
if( divList[j].id == result[i].id )
divList[j].div.appendChild( createElementX( " ("+result[i].fav_count+" fav"+( result[i].fav_count == 1 ? ")" : "s)" ) ) );
}
});
}
}
/**
Blacklist implementation that replaces images (full and thumbnail) with links labeled with tags
they matched in the blacklist. Clicking the link/image alternates between the two. Translation notes
appear every other time the image is displayed.
User and rating metatags are supported. Your own uploads are exempt from the blacklist.
blacklist: regex string containing the blacklisted tags
*/
function blacklistTags( blacklistStr )
{
if( recursion )
return;
var blacklist;
try{ blacklist = new RegExp( "(^|.* )("+blacklistStr+")( .*|$)", "i" ); }
catch(e){ throw "Unable to parse blacklist regex"; }
var imageContainer = document.getElementById("image-container");
var noteContainer = imageContainer && document.getElementById("note-container");
setTimeout( function()
{
if( imageContainer )
{
var user = document.evaluate("//section/ul/li[contains(text(),'Uploader')]/a", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue.textContent;
var rating = document.evaluate("//section/ul/li[contains(text(),'Rating:')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue.textContent;
rating = rating.indexOf("Safe") > 0 ? 's' : ( rating.indexOf("Explicit") > 0 ? 'e' : 'q' );
applyBlacklist( imageContainer, document.getElementsByName("tags")[0].content, user, rating );
}
MS_observeInserts( function(e)
{
var found = [];
if( !e )
found = document.getElementsByClassName("post-preview");
else if( e.target.className && e.target.className.indexOf("post-preview") >= 0 )
found = [ e.target ];
else if( e.target.getElementsByClassName )
found = e.target.getElementsByClassName("post-preview");
for( var i = 0; i < found.length; i++ )
applyBlacklist( found[i], found[i].getAttribute("data-tags"), found[i].getAttribute("data-uploader"), found[i].getAttribute("data-rating") );
})();
}, 10 );
function applyBlacklist(elem, tags, user, rating)
{
if( !elem || !tags || user == login )
return;
tags += " user:"+user+" rating:"+rating;
var badTags = [];//List of all of this post's tags that hit the blacklist
while( blacklist.test(tags) )
{
badTags.push( tags.replace( blacklist, '$2' ) );
tags = tags.replace( blacklist, '$1 $3' );
}
badTags.sort();
var image = badTags.length && elem.getElementsByTagName("img")[0];
if( !image )
return;
var blackLink = createElementX({ tag: "a", href:image.src, text:('hidden ('+badTags+')').replace(/,/g,', '), title:( image.title || "" ) });
if( elem.className )
{
if( elem.className.indexOf("deleted") > 0 )
blackLink.style.color = "#000";
else if( elem.className.indexOf("pending") > 0 )
blackLink.style.color = "#00F";
else if( elem.className.indexOf("flagged") > 0 )
blackLink.style.color = "#F00";
else if( elem.className.indexOf("parent") > 0 )
blackLink.style.color = "#CC0";
else if( elem.className.indexOf("child") > 0 )
blackLink.style.color = "#0F0";
}
if( image.parentNode.href )
{
//Thumbnails are surrounded by links. Hide those instead of the images themselves.
image = image.parentNode;
blackLink.href = image.href;
//Comments have div.post-preview/div.preview/a/img
elem = image.parentNode;
elem.style.textAlign = "center";
}
var showNotes = true;
if( elem == imageContainer )
noteContainer.style.display = "none";
image.style.display = "none";
elem.insertBefore( blackLink, elem.firstChild );
blackLink.addEventListener( "click", function(e) { if( !e.ctrlKey ) e.preventDefault(); }, false );
elem.addEventListener( "click", function(e)
{
if( e.ctrlKey ) return;
e.preventDefault();
//Swap styles
var oldImageStyle = image.style.display || "none";
image.style.display = blackLink.style.display || "block";
blackLink.style.display = oldImageStyle;
if( elem == imageContainer )
noteContainer.style.display = ( image.style.display == "none" || (showNotes = !showNotes) ? "none" : "block" );
}, true );
}
}
/**
Sets all times to the "X units ago" form with custom precision.
*/
function precisionTime(precision)
{
if( recursion )
return;
MS_observeInserts( function(e)
{
if( e && !e.target.getElementsByTagName )
return;
var timeNodes = ( e ? e.target : document ).getElementsByTagName("time");
for( var i = 0; i < timeNodes.length; i++ )
{
//<time datetime="2013-02-26T22:16-05:00" title="2013-02-26 22:16:25 -0500">2013-02-26 22:16</time>
var unit = "", title = timeNodes[i].getAttribute("title").split(" ");
var duration = ( Date.now() - new Date(title[0]+'T'+title[1]+title[2]) ) / 1000;
//new Date(timeNodes[i].getAttribute("datetime")).getTime() - 1000*parseInt( (timeNodes[i].getAttribute("title") || "0 0:0:0").split(/[\s:]+/,4)[3], 10 );
if( duration < 60 )
unit = "second";
else if( (duration /= 60) < 60 )
unit = "minute";
else if( (duration /= 60) < 24 )
unit = "hour";
else if( (duration /= 24) < 30.4375 )
unit = "day";
else if( (duration /= 30.4375) < 12 )
unit = "month";
else if( (duration /= 12) > 0 )
unit = "year";
else continue;//Parsing error
if( precision > 0 && unit != "second" )
timeNodes[i].textContent = duration.toFixed(precision)+" "+unit+"s ago";
else
{
duration = ( duration <= 0 ? "0" : duration.toFixed(0) );
timeNodes[i].textContent = duration+" "+unit+( duration == "1" ? " ago" : "s ago" );
}
}
})();
}
/**
Adds a pseudo-tag subscription to your profile.
title: Title of tag subscription; must be unique
useTimestamp: If true, append a timestamp to the title with each refresh
refreshInterval: Hours to wait before refreshing the subscription
maxThumbs: Number of thumbnails to display
isIntersect: If true, find posts that contained in all the queries. Otherwise, find posts in any query.
queryList [ { search:"", pages#, filter:[""] }, ... ]
The optional filter parameter finds the intersection of just that query and the tags in the filter.
Metatags and negated tags are not supported in the filter.
If isIntersect is true, posts will be in the order of the final query.
Otherwise, posts are sorted by descending ID.
The subscription title points to a tag search using the tags passed to the first query.
When the subscription refreshes, all queries will be rerun with every page load until all
have completed on the same page.
*/
function addTagSubscription( title, useTimestamp, refreshInterval, maxThumbs, isIntersect, queryList )
{
//Validate arguments
if( recursion || !login )
return;
if( arguments.length == 1 && title instanceof Array )
{
for( var i = 0; i < title.length; i++ )
addTagSubscription( title[i].title, title[i].timestamp, title[i].refresh, title[i].thumbs, title[i].intersect, title[i].queries );
return;
}
if( !title )
throw "No title";
if( refreshInterval <= 0 )
throw ""+title+": Invalid refresh interval";
if( !(queryList instanceof Array) || !queryList.length )
throw ""+title+": Invalid query list";
if( maxThumbs <= 0 || maxThumbs > 100 )
maxThumbs = 10;
//Validate queries
for( var i = 0; i < queryList.length; i++ )
{
if( typeof(queryList[i].search) != "string" )
throw ""+title+": 'search' parameter missing or not a string";
if( !queryList[i].pages )
queryList[i].pages = 1;//Missing number of pages, assume 1.
if( queryList[i].pages > 1000 )
queryList[i].pages = 1000;//Limit 1000 pages (100k posts)
if( !queryList[i].filter )
queryList[i].filter = [];
}
var varPrefix = "addTagSubscription."+title;
var subDiv = false;
//Add the DIV to contain the thumbs to your own user page and append the thumbs if they already exist
var userContent = document.getElementsByClassName("user-statistics").length && document.getElementById("a-show");
if( userContent && ( location.pathname == "/users/"+loginID || userContent.getElementsByTagName("h1")[0].textContent == login ) )
{
subDiv = createElementX( { tag: "div", class:"box", id: varPrefix+"_thumbList" } );
var thumbList = MS_getValue( varPrefix+"_thumbList" );
if( thumbList )
subDiv.innerHTML = thumbList;
else
{
subDiv.appendChild( createElementX({ tag:"h2", title:JSON.stringify(queryList) }) ).appendChild( createElementX({ tag:"a", href:"/posts?tags="+queryList[0].search.replace(/ +/,'+'), text:title }) );
if( MS_getValue( varPrefix+"_lock" ) )
MS_setValue( varPrefix+"_lock", true, 30 );
}
userContent.appendChild(subDiv);
}
if( MS_getValue( varPrefix+"_lock" ) )
return;
var postResults = [], currentResults = [];
runQuery(0, 1);
function runQuery( argIndex, page )
{
//Set 10 second lock every time a query runs, to prevent concurrent queries
MS_setValue( varPrefix+"_lock", true, 10 );
if( argIndex >= queryList.length )
return subBuildThumbs();//No more arguments, start building
if( page == 1 )
currentResults = [];//First page, no results yet
if( page > queryList[argIndex].pages )
{
//No more pages.
if( argIndex == 0 || !isIntersect )
postResults = postResults.concat( currentResults );
else
{
//Find the intersection between the previous queries and this one, using the order of this one.
var oldPostResults = postResults;
postResults = [];
for( var i = 0; i < currentResults.length; i++ )
{
for( var j = 0; j < oldPostResults.length; j++ )
if( oldPostResults[j]["id"] == currentResults[i]["id"] )
{
postResults.push( oldPostResults[j] );
break;
}
}
if( postResults.length == 0 )
argIndex = queryList.length;//Intersection with empty set :(
}
return runQuery( argIndex + 1, 1 );
}
//Fetch next page
MS_requestAPI( "/posts.json?tags="+queryList[argIndex].search.replace(/ +/g,'+')+"&page="+page, function(responseDetails)
{
var result;
try { result = MS_parseJSON(responseDetails.responseText); }
catch(e) { return runQuery( argIndex, page ); }
if( queryList[argIndex].filter.length == 0 )
currentResults = currentResults.concat( result );
else for( var i = 0; i < result.length; i++ )
{
//Only add posts that contain all of the tags in the filter
var pTags = result[i]["tag_string"].toLowerCase().split(' '), lastIdx = 0;
for( var j = 0; lastIdx >= 0 && j < queryList[argIndex].filter.length; j++ )
{
if( queryList[argIndex].filter[j].indexOf("user:") != 0 )
lastIdx = pTags.indexOf( queryList[argIndex].filter[j] );
else if( queryList[argIndex].filter[j].replace(/_/g,' ').toLowerCase() != "user:"+result[i]["uploader_name"].toLowerCase() )
lastIdx = -1;
}
if( lastIdx >= 0 )
currentResults.push( result[i] );
}
//Go to next page if less than the maximum amount was returned, or if we have enough thumbs already
if( result.length < 100 || ( queryList.length == 1 && currentResults.length > maxThumbs ) )
page = 9999;
//Get next page
runQuery( argIndex, page + 1 );
});
}
function subBuildThumbs()
{
//Sort union subscriptions by decreasing post ID
if( !isIntersect && queryList.length > 1 )
postResults.sort( function(a,b) { return( b["id"] - a["id"] ); } );
//Remove duplicates up to the point where it doesn't matter
for( var i = 1; i < postResults.length && i < maxThumbs; i++ )
{
for( var j = i - 1; j > 0; j-- )
if( postResults[j]['id'] == postResults[i]['id'] )
{
//An earlier post already has this ID, so remove this post
postResults.splice( i--, 1 );
break;
}
}
if( subDiv )
subDiv.innerHTML = "";//Remove the old title that may have been added at the start
else
subDiv = createElementX({ tag:"div", class:"box" });//Dummy DIV
function zeroIt(a) { return( a < 10 ? "0"+a : a ); }
var tdate = new Date();
var timestamp = " ("+(tdate.getMonth()+1)+"-"+zeroIt(tdate.getDate())+" "+zeroIt(tdate.getHours())+":"+zeroIt(tdate.getMinutes())+")";
subDiv.appendChild( createElementX({ tag:"h2", title:JSON.stringify(queryList)+timestamp }) ).appendChild( createElementX([ { tag:"a", href:"/posts?tags="+queryList[0].search.replace(/ +/,'+'), text:title }, ( useTimestamp ? timestamp : "" ) ]) );
subDiv.appendChild( createElementX({ tag:"div", class:"box" }) ).appendChild( buildThumbs( postResults, maxThumbs ) );
MS_setValue( varPrefix+"_thumbList", subDiv.innerHTML, 7*24*3600 );
MS_setValue( varPrefix+"_lock", true, refreshInterval * 3600 );
}
}
/**
Runs a truncated "Find artist" search on posts with URL sources but no artist tag and displays the results
below the source, along with the number of other posts returned by a source search.
*/
function sourceStatLinks( findArtist, findSource )
{
var source = !recursion && document.getElementById("post_source");
var sourceStat = source && source.value.indexOf("http") == 0 && document.evaluate("//aside[@id='sidebar']/section/ul/li[contains(text(),'Source:')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if( !sourceStat )
return;
source = source.value;
var postSearch = "tags=status:any+source:"+source.replace( /[^\/]+$/, '' )+"*";
var pixivArtist = 0, statLink = sourceStat.childNodes[1];
if( statLink.href && /\.pixiv.net\/(img|img[0-9]+\/img)\/[^ \/]+\/[0-9]+/.test(source) )
{
pixivArtist = source.replace(/(.*\/img\/|\/.*)/g,'');
postSearch = "tags=status:any+source:pixiv/"+pixivArtist+"/*";
//statLink.href += '#'+pixivArtist;
}
var findArtistDiv, sourceSearchDiv;
if( (findArtist || findSource) && !document.getElementById("tag-list").getElementsByClassName("category-1")[0] )
{
findArtistDiv = createElementX({tag:"div", style:"margin-left:1.5em; word-wrap:break-word; display:none" });
sourceSearchDiv = createElementX({tag:"div", style:"margin-left:1.5em; word-wrap:break-word; display:none" });
sourceStat.parentNode.insertBefore( createElementX([ findArtistDiv, sourceSearchDiv ]), sourceStat.nextSibling );
if( findArtist )
artistSearch();
if( findSource )
sourceSearch();
}
function artistSearch()
{
MS_requestAPI( '/artists.json?search[name]='+source, function(responseDetails)
{
var result;
try{ result = MS_parseJSON(responseDetails.responseText); }
catch(e){ return artistSearch(); }
if( result.length < 4 )
{
for( var i = 0; i < result.length; i++ )
{
findArtistDiv.innerHTML += '<li class="category-1"><a href="/artists/show_or_new?name='+result[i]["name"]+'" onclick="document.getElementById(\'post-edit-link\').click(); Danbooru.RelatedTag.toggle_tag({target:this,preventDefault:function(){}}); return false;">'+result[i]["name"]+'</a></li>';
}
findArtistDiv.style.display = "block";
}
});
}
//Source search, clipping off the filename
function sourceSearch()
{
MS_requestAPI( "/posts.json?"+postSearch, function(responseDetails)
{
var result;
try{ result = MS_parseJSON(responseDetails.responseText); }
catch(e){ return sourceSearch(); }
if( result.length == 100 )
result = "At least 99 other posts";
else if( result.length == 2 )
result = "1 other post";
else
result = ( result.length ? result.length - 1 : 0 )+" other posts";
sourceSearchDiv.innerHTML = '<a href="/posts?'+postSearch+'">'+result+'</a>';
sourceSearchDiv.style.display = "block";
});
}
}
/**
Applies a color the first time a downvote link is clicked, only letting the vote through
if it (and nothing else) is clicked again.
*/
function doubleDown(color)
{
if( recursion || !color || !content )
return;
MS_observeInserts( function(e)
{
var down = document.evaluate(".//li/a[@data-method and contains(@href,'votes?score=down')]", (e ? e.target : document), null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for( var i = 0; i < down.snapshotLength; i++ )
{
var sLink = down.snapshotItem(i);
var fLink = createElementX({ tag:"a", href:"#", text:sLink.textContent, onclick:"return false;", title:"Click twice to vote down." });
sLink.parentNode.insertBefore( fLink, sLink );
sLink.style.display = 'none';
sLink.style.color = color;
content.addEventListener("click", (function(firLink,secLink){ return function(e)
{
if( e && e.target == firLink )
{
firLink.style.display = "none";
secLink.style.display = "inline";
}
else
{
firLink.style.display = "inline";
secLink.style.display = "none";
}
}; })( fLink, sLink ), false );
}
} )();
}
//=====================================================================================
//=============================== HELPER FUNCTIONS ====================================
//=====================================================================================
function nbsp(count)
{
var result = "";
while( count-- > 0 )
result += "\u00a0";
return result;
}
function createElementX(obj)
{
if( arguments.length > 1 || (arguments = obj) instanceof Array )
{
var fragment = document.createDocumentFragment();
for( var i = 0; i < arguments.length; i++ )
fragment.appendChild( createElementX( arguments[i] ) );
return fragment;
}
if( obj instanceof Element )
return obj;
else if( typeof(obj) == "string" )
return document.createTextNode(obj);
else if( !obj.tag )
{
if( obj.childNodes )
return obj;
if( elem in obj )
return document.createTextNode(""+obj);
return document.createDocumentFragment();// {}
}
var elem = document.createElement(obj.tag);
for( var key in obj )
{
if( key == "text" )
elem.textContent = obj[key];
else if( key != "tag" )
elem.setAttribute(key, obj[key]);
}
return elem;
}
function MS_setValue( key, value, expires, memObj )
{
//If 0 or less, value never expires
if( expires > 0 )
expires = Math.floor( new Date().getTime()/1000 + expires );
else
expires = 0;
var i, keyList = [];
if( !memObj )
{
GM_setValue( key, JSON.stringify({ "value": value, "expires": expires }) );
try{ keyList = MS_parseJSON( GM_getValue( "Miscellaneous.keyList" ) ) || []; }
catch(err){ keyList = []; }
}
else
{
keyList = memObj.keyList = MS_parseJSON( memObj.keyList ) || [];
memObj[key] = { "value": value, "expires": expires };
MS_setMemObj( memObj, "MS_setValue" );
}
if( !(keyList instanceof Array) )
throw "MS_setValue(): Invalid keyList ("+typeof(keyList)+"): "+keyList;
//Find insertion point, deleting duplicates along the way
for( i = 0; i < keyList.length && keyList[i].expires <= expires; i++ )
if( keyList[i].key == key )
keyList.splice( i--, 1 );
keyList.splice( i, 0, { "key":key, "expires":expires });
if( !memObj )
GM_setValue( "Miscellaneous.keyList", JSON.stringify(keyList) );
}
function MS_getValue( key, defaultValue, memObj )
{
if( !memObj && typeof(defaultValue) == "object" && defaultValue.saveName )
{
memObj = defaultValue;
defaultValue = null;
}
if( memObj )
{
if( !memObj[key] || ( memObj[key].expires > 0 && memObj[key].expires < new Date().getTime()/1000 ) )
return defaultValue;
return memObj[key].value;
}
var value = GM_getValue( key );
if( !value )
return defaultValue;
try{ value = MS_parseJSON( value ); }
catch(e) { return defaultValue; }
if( value.expires > 0 && value.expires < new Date().getTime()/1000 )
return defaultValue;
return value["value"];
}
function MS_deleteValue( key, memObj )
{
var keyList;
if( memObj )
keyList = memObj.keyList = MS_parseJSON( memObj.keyList ) || [];
else
{
try{ keyList = MS_parseJSON( GM_getValue( "Miscellaneous.keyList" ) ) || []; }
catch(err){ keyList = []; }
}
if( !(keyList instanceof Array) )
throw "MS_deleteValue(): Invalid keyList ("+typeof(keyList)+"): "+keyList;
var now = new Date().getTime()/1000;
for( var i = keyList.length - 1; i >= 0; i-- )
if( keyList[i].key == key )
{
keyList.splice(i,1);
if( !memObj )
{
GM_deleteValue( key );
GM_setValue( "Miscellaneous.keyList", JSON.stringify(keyList) );
}
else
{
delete memObj[key];
MS_setMemObj( memObj, "MS_deleteValue" );
}
}
}
function MS_clearValues(num, memObj)
{
if( num <= 0 )
return;
var keyList;
if( memObj )
keyList = memObj.keyList = MS_parseJSON( memObj.keyList ) || [];
else
{
try{ keyList = MS_parseJSON( GM_getValue( "Miscellaneous.keyList" ) ) || []; }
catch(err){ keyList = []; }
}
var now = new Date().getTime()/1000;
var oldLen = keyList.length;
for( var i = 0; num > 0 && i < keyList.length && keyList[i].expires < now; i++ )
if( keyList[i].expires > 0 )
{
num--;
GM_deleteValue( keyList.splice( i--, 1 )[0].key );
}
if( oldLen != keyList.length && !memObj )
GM_setValue( "Miscellaneous.keyList", JSON.stringify(keyList) );
} MS_clearValues(1);
function MS_setMemObj(obj,func)
{
if( obj._timer != null )
clearTimeout( obj._timer );
obj._timer = setTimeout( function()
{
delete obj._timer;
MS_clearValues( 20, obj );
MS_setValue( obj.saveName, obj, obj.expires );
}, 1500 );
}
function MS_parseJSON(text)
{
if( text instanceof Array )
{
//Parse all elements in arrays of strings
for( var i = 0; i < text.length && typeof(text[i]) == "string"; i++ )
try{ text[i] = MS_parseJSON( text[i] ); }
catch(err){ throw "Bad JSON in text["+i+"]: "+text[i]; }
return text;
}
else if( typeof(text) == "string" ) return JSON.parse(text, function(key,val)
{
//Recursively parse strings that look like objects
if( typeof(val) == "string" )
{
try{ return MS_parseJSON(val); }
catch(err){}
}
return val;
});
//Not a string or array, so return unchanged
return text;
}
function MS_addStyle(text)
{
if( text instanceof Array )
{
for( var i = 0; i < text.length; i++ )
MS_addStyle( text[i].style ? text[i].style : text[i] );
}
else if( typeof(text) == "string" )
{
var style = createElementX({ tag:'style', type:'text/css' });
style.innerHTML = text.replace(/;/g,'!important;')
.replace(/(!\s*important\s*)+\}/gi,'}')//Remove existing instances of "!important" to avoid duplicates
.replace(/\}/gi,'!important}');//Add "!important" to everything
document.getElementsByTagName('head')[0].appendChild(style);
}
else throw "MS_addStyle(): Invalid type: "+text;
}
function MS_alert( messageP )
{
//Creates a large "alert" box that collects multiple alerts and doesn't pause the script
var showAlerts = true;
var dialog = document.body.appendChild( createElementX({ tag:"div", style:"position:fixed; left:10%; top:10%; z-index:10; background-color:white; display:none; width:20em; height:20em; border:2px solid black; padding:20px 20px 20px 20px; overflow:auto" }) );
var header = dialog.appendChild( createElementX({ tag:"h4", text: "Miscellaneous Tweaks " }) );
dialog.appendChild( createElementX({ tag:"hr" }) );
var text = dialog.appendChild( createElementX({ tag:"div" }) );
header.appendChild( createElementX({ tag:"input", type:"button", value:"Clear", title:"Clear all warnings and hide this alert." }) ).addEventListener( "click", function(){ dialog.style.display = "none"; text.innerHTML = ""; } );
header.appendChild( createElementX(" ") );
header.appendChild( createElementX({ tag:"input", type:"button", value:"Hide", title:"Hide this alert without clearing its contents." }) ).addEventListener( "click", function(){ dialog.style.display = "none"; } );
header.appendChild( createElementX(" ") );
header.appendChild( createElementX({ tag:"input", type:"button", value:"Stop", title:"Stop additional alerts from being triggered." }) ).addEventListener( "click", function(){ showAlerts = false; } );
MS_alert = function(message)
{
if( showAlerts )
{
text.appendChild( createElementX( ""+message, { tag:"hr" }) );
dialog.style.display = "block";
dialog.style.width = "80%";
dialog.style.height = "60%";
}
}
MS_alert(messageP);
}
function MS_observeInserts(funcP)
{
if( !content || recursion )
return funcP;
//Listen to node insertions only under id=content node
var funcList = [ ];
function mutateManager(e)
{
for( var i = 0; i < funcList.length; i++ )
{
try{ funcList[i](e); }
catch(err){ GM_log("Tweaks - Insertion error - "+funcList[i].toString().replace(/\(.*/,'()')+": "+err); }
}
}
var MutObj = window.MutationObserver || window.WebKitMutationObserver;//Firefix and Chrome but not Opera
if( MutObj )
{
var monitor = new MutObj( function(mutationSet){
mutationSet.forEach( function(mutation){
for( var i = 0; i < mutation.addedNodes.length; i++ )
if( mutation.addedNodes[i].nodeType == 1 )
mutateManager({ "target":mutation.addedNodes[i] });
});
});
monitor.observe( content, { childList:true, subtree:true } );
}
else content.addEventListener( "DOMNodeInserted", mutateManager, false );
MS_observeInserts = function(func)
{
if( !content || recursion )
return function(){};
funcList.push( func );
return func;
};
return MS_observeInserts(funcP);
}
/** Makes request to API link, e.g. "/posts.json". If "limit=" isn't included, limit=100 is added to the end. */
function MS_requestAPI( link, loadFun, errFun )
{
GM_xmlhttpRequest(
{
method: "GET",
url:location.protocol+"//"+location.host+link+( link.indexOf("limit=") < 0 ? ( link.indexOf("?") > 0 ? "&limit=100" : "?limit=100" ) : "" ),
onload:loadFun,
onerror:( errFun ? errFun : function(err){ MS_requestAPI( link, loadFun ); } )
});
}
/** Helper method to construct a document fragment of thumbnails from post API results */
function buildThumbs(json, limit)
{
if( limit <= 0 )
limit = 100;
var i, result = document.createDocumentFragment();
if( typeof(json) == "string" )
try{ json = MS_parseJSON(json); } catch(e){ return result; }
for( i = 0; limit-- > 0 && i < json.length; i++ )
{
var borderColors = [], imgStyle = "", status = "post-preview", flag = "";
if( json[i].has_children )
{
borderColors.push("#0F0");
status += " post-status-has-children";
}
if( json[i].parent_id )
{
borderColors.push("#CC0");
status += " post-status-has-parent";
}
if( json[i].is_deleted )
{
borderColors.push("#000");
status += " post-status-deleted";
flag = "deleted";
}
else if( json[i].is_pending )
{
borderColors.push("#00F");
status += " post-status-pending";
flag = "pending";
}
else if( json[i].is_flagged )
{
borderColors.push("#F00");
status += " post-status-flagged";
flag = "flagged";
}
if( borderColors.length > 1 )
{
// app/assets/javascripts/posts.js
imgStyle = "border-color: "
if( borderColors.length == 3 )
imgStyle += borderColors[0]+" "+borderColors[2]+" "+borderColors[2]+" "+borderColors[1];
else
imgStyle += borderColors[0]+" "+borderColors[1]+" "+borderColors[1]+" "+borderColors[0];
//$img.css("border", "2px solid");
}
var article = createElementX({ "tag":"article",
"class":status,
"id":"post_"+json[i].id,
"data-id":json[i].id,
"data-tags":json[i].tag_string,
"data-uploader":json[i].uploader_name,
"data-rating":json[i].rating,
"data-width":json[i].image_width,
"data-height":json[i].image_height,
"data-flags":flag,
"data-parent-id":( json[i].parent_id || "" ),
"data-has-children":json[i].has_children,
"data-score":json[i].score,
"data-favcount":json[i].fav_count });
article.appendChild( createElementX({ tag:"a", href:"/posts/"+json[i].id }) ).appendChild( createElementX({ tag:"img", src:"/ssd/data/preview/"+json[i].md5+".jpg", alt:json[i].tag_string, title:json[i].tag_string+" user:"+json[i].uploader_name+" rating:"+json[i].rating+" score:"+json[i].score, style:imgStyle }) );
result.appendChild(article);
}
return result;
}