4chan utility
Provides additional functionalities for 4chan
This script is marked as "external"This script was commisioned by someone else, and I will not check if it works. If it breaks,
contact me Click here to install
Browse More Scripts
// ==UserScript==
// @name 4chan utility
// @namespace 5a4825a9eae937cae9cdda4aabc758da1332d720
// @version 1.51
// @description Provides additional functionalities for 4chan
// @author /u/AyrA_ch
// @match http://boards.4chan.org/*/thread/*
// @match https://boards.4chan.org/*/thread/*
// @grant GM_download
// @grant GM_info
// @grant GM_getValue
// @grant GM_setValue
// @connect self
// @connect i.4cdn.org
// @external true
// ==/UserScript==
//How to use
//==========
//At the very bottom of threads, there is a "4chan Utility Settings" link
//Features
//========
//Options with "*" can be toggled
// - Filter: File names and messages can be hidden according to filters. Either as a text search or regular expression
// - Drag & Drop: You can drag a file onto the site and it will open the reply dialog with said file attached
// - Larger Reply dialog: The reply dialog window is larger by default
// - Automatic Pass posting: Automatically adds "since4pass" into the subject line if you are a pass user
// - Keyboard shortcut: Pressing [CTRL]+[ENTER] will post your reply
// - Keyboard shortcut: [UP] and [DOWN] Select a reply, [ENTER] will expand or shrink the associated image.
// - Constant updater: Updater updates constantly every 60 seconds
// - Download links: Links to post images behave as download links and preserve the file name
// - Hide/unhide last clicked post by pressing [H]
// - Display link of known source if the image file name format is known
// - * link tags: Download links are tagged with a floppy disk icon
// - * Tab links: Links that open in a new tab have an arrow attached
// - * Checkerboard: Transparent images are put in front of a checkerboard pattern to make the transparency visible
// - * Stats Color: Stats are highlighted if a thread is expired
// - * Auto Retry: Retries posting after 5 seconds on error.
//Changelog
//=========
//1.51 - Add e621 file detection
//1.50 - Setting to remove width limitations on posts
//1.49 - Can now directly link to external sources depending on the image file name (includes FA).
//1.48 - Disable lazy loading of thumbnails
//1.47 - Regex now case sensitive
//1.46 - Allow regex modifiers
//1.45 - Remove Party hat
//1.44 - MD5 Hash Blocking
//1.43 - [INSERT]: Reply
//1.42 - [HOME]+[END]: Select First/Last post
//1.41 - Fix incomplete processing of expired threads
//1.40 - Add debug logging
//1.39 - Retry Post option for failed posts
//1.38 - Automatically select post from ID in url if given
//1.37 - Shrunk settings box
//1.36 - More keyboard navigation (arrows + enter)
//1.35 - Implement quick hide/unhide key
//1.34 - Cleanup and reordering of code
//1.33 - Fix download settings message color
//1.32 - Provide a way to unhide and reapply filters
//1.31 - Prevent re-collapsing of automatically filtered posts if manually expanded again
//1.30 - Easter 2018
//1.29 - Fix comment box resizer
//1.28 - Fix download links being added to "Close" webm buttons
//1.27 - Default settings to "yes"
//1.26 - Coloring of image/post stats if limit reached
//1.25 - Styles are now settings
//1.24 - proper Download feature. If enabled, shows Icon
//1.23 - Make settings persistent on browser data clear
//1.22 - No longer remove target="_blank" from links but add it
//1.21 - Rules support Regex now
//1.20 - Configurable hiding rules
//1.19 - Focus text box when dragging files
//1.18 - Detect and ignore non-file drag. This allows text dragging again
//1.17 - Can drag and drop files onto the page to upload them now
//1.16 - Fix dead thread detection for edge cases
//1.15 - Detect dead thread
//1.14 - Fix URL parser because 4chan needs to update perfectly working stuff all the time.
//1.13 - Add more keywords for automatic hiding
//1.12 - Add background image so transparent parts of images are visible
//1.11 - Decode URLs in the no-dereferrer feature
//1.10 - Fix bug in the no-dereferrer feature
//1.9 - Remove "nofollow" from fixLinks() as it is useless in this context
//1.8 - Removed the (You) from the CSS because 4chan reverted the change
//1.7 - Fixed buggy 4chan-pass detection
//1.6 - Check if CSS is different and apply if so
//1.5 - Automatic 4chan-pass detection
//1.4 - Ensured the CSS update from 1.3 is actually applied
//1.3 - Updated CSS to bring back the "(You)"
//1.2 - Added automatic thread hiding and removed halloween css
//1.1 - Updated because 4chan changed some scripts
//1.0 - Initial release
(function () {
'use strict';
//Set this to the seconds you want the updater to check for new replies.
//Do not set too low or they may block your access.
var UPDATE_INTERVAL = 60;
//Interval to use after making a reply.
//Every time it expires, 10 seconds will be added until UPDATE_INTERVAL is reached again.
var REPLY_INTERVAL = 10;
//Set this to true, if you are a 4chan pass user and want the "since4pass" in the text box automatically
//Default is to autodetect
var PASSUSER = (document.cookie.split("; ").indexOf("pass_enabled=1") > -1);
//Allows submitting your reply with [CTRL]+[ENTER]
var QUICKSEND = true;
//Tag for automatically processed replies. Change only if it conflicts
var CLASS_PROCESSED = "fch-processed";
//Supported site links to add to the
var SUPPORTED_SITES = [{
name: "Furaffinity",
match: /^\d+\.([^_]+)_.+$/,
replace: "https://furaffinity.net/user/$1",
img: "https://furarchiver.net/favicon.ico"
},
{
name: "e621",
match: /^([A-F\d]{32})\.\w+$/i,
replace: "https://e621.net/posts?md5=$1",
img: "https://e621.net/favicon.ico"
}
];
//No changes needed below
console.log("Initializing", GM_info.script.name, "Version:", GM_info.script.version);
console.log("Enable debug logging in your browser for more information");
//Automatic post hiding
//=====================
//A post that matches at least one rule will be hidden automatically.
//The rules are partial matches and case insensitive.
var blacklist_data = GM_getValue("blacklist", "");
if (blacklist_data) {
try {
blacklist_data = JSON.parse(blacklist_data);
} catch (e) {
blacklist_data = null;
}
}
if (!blacklist_data) {
blacklist_data = {
file: [],
msg: []
};
GM_setValue("blacklist", JSON.stringify(blacklist_data));
} else {}
console.debug("Loaded Blacklist:", blacklist_data);
var filenames = blacklist_data.file;
var messages = blacklist_data.msg;
var q = document.querySelector.bind(document);
var qa = document.querySelectorAll.bind(document);
var counter = UPDATE_INTERVAL;
var IMG_SRC = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA" +
"AABAAAAAQCAIAAACQkWg2AAAAJ0lEQVR42mOcOXMmAzZw9uxZrOKM" +
"oxpooiEtLQ2rhLGx8agG+mkAACpiL/lWCxuBAAAAAElFTkSuQmCC";
//Creates a HTML element
var ce = function (tagName, attr, HTML) {
var ele = document.createElement(tagName);
if (attr) {
Object.keys(attr).forEach(function (v) {
ele.setAttribute(v, attr[v]);
});
}
if (HTML) {
ele.innerHTML = HTML;
}
return ele;
};
//custom thread updater that always goes for a constant number of seconds
var updateLoop = {
timeout: UPDATE_INTERVAL
};
updateLoop.id = window.setInterval(function () {
var box = q("#cbUpdate");
//Website not ready if this is missing
if (!box) {
return;
}
replaceTitle();
downloadLink();
var field = box.parentNode.nextElementSibling;
if (q("link[rel='shortcut icon']").href.indexOf("deadthread.ico") > 0) {
if (field) {
field.textContent = "Thread expired.";
}
box.checked = false;
box.setAttribute("disabled", "disabled");
window.clearInterval(updateLoop.id);
console.debug("Thread expired. Stopping updates");
return;
}
if (box.checked && --counter <= 0) {
updateLoop.timeout = counter = Math.min(updateLoop.timeout + 10, UPDATE_INTERVAL);
console.debug("Updating thread");
ThreadUpdater.update();
}
if (field) {
field.textContent = counter + " seconds";
}
}, 1000);
//replaces the title. Essentially Removes the board name and some spacing
var replaceTitle = function () {
var title = document.title;
var r = /(\/\w*?\/\s-\s)/;
var n = /\(\d*?\) \/\w*?\//;
if (title.length - title.lastIndexOf(" - 4chan") == 8) {
//remove "4chan"
title = title.substr(0, title.lastIndexOf(" - ")).trim();
//remove forum name
title = title.substr(0, title.lastIndexOf(" - ")).trim();
}
//kill leading dashes
while (title.indexOf("-") === 0) {
title = title.substr(1).trim();
}
//remove preceding numbers
if (title.match(n)) {
title = title.replace(n, "");
}
//remove preceding forum name
if (title.match(r)) {
title = title.replace(r, "");
}
if (title && document.title != title) {
document.title = title;
console.debug("Updating title. New Value:", title);
}
};
//This would set the custom CSS.
//It is not needed anymore as we use 4chan's user css mechanism.
//It's still here in case the feature ever gets dropped.
var addCSS = function () {
//var ele=document.createElement("style");
//ele.innerHTML="a[target=_blank]::before{content:\"⤴\";}";
//q("head").appendChild(ele);
};
//replace the existing update boxes with ours and check them by default
var updateFunction = function () {
var boxes = qa("input[data-cmd=auto]");
if (boxes && boxes.length > 1) {
var newBox = ce("input", {
type: "checkbox",
id: "cbUpdate",
checked: "true"
});
boxes[0].parentNode.remove();
boxes[1].parentNode.replaceChild(newBox, boxes[1]).remove();
}
};
//Make the file name a download link and show the full image name.
var downloadLink = function () {
//this gets all filename links
var ele = qa(".fileText a");
for (var i = 0; i < ele.length; i++) {
var e = ele[i];
//If it has the download attribute, we already processed it
if (!e.getAttribute("download") && e.href && e.href.match(/\/i\w*\.4c(dn|han)\.org\//)) {
//Try to force a download using the extension instead of browser mechanics
e.addEventListener("click", function (evt) {
if (GM_info.downloadMode === "browser") {
evt.preventDefault();
evt.stopPropagation();
GM_download({
url: this.href,
name: this.getAttribute("download")
});
}
});
//add the "target=_blank" attribute.
e.setAttribute("target", "_blank");
//The title has the full name. Replace the link text with that
if (e.getAttribute("title")) {
e.setAttribute("download", e.getAttribute("title"));
e.textContent = decodeURIComponent(e.getAttribute("title"));
} else {
//No title present. Just use the text of the link instead.
e.setAttribute("download", e.textContent);
}
SUPPORTED_SITES.forEach(function (v) {
var m = e.textContent.match(v.match);
if (m) {
var url = e.textContent.replace(v.match, v.replace);
var a = document.createElement("a");
var img = document.createElement("img");
var space = document.createTextNode(" | ");
img.width = img.height = 12;
img.src = v.img;
a.href = url;
a.classList.add("external-site");
a.target = "_blank";
a.title = v.name;
a.appendChild(img);
//Insert link after the download link
e.parentNode.insertBefore(a, e);
e.parentNode.insertBefore(e, a);
e.parentNode.insertBefore(space, a);
}
});
console.debug("processed download link", e);
} else {}
}
};
//remove unnecessary URL segments
var fixUrl = function () {
//return #bottom if the "#..." is not in the URL
var hash = function () {
if (!location.hash || location.hash === "#") {
return "#bottom";
}
return location.hash;
};
//Grab the url segments and extract board and id
var segments = location.pathname.substr(1).split('/');
var board = segments[0];
var id = segments[2];
//kill everything after ID and navigate. This is a pure cosmetical filter,
//but shorter URLs are better for sharing.
if (segments.length > 3) {
location.replace(location.origin + "/" + segments.slice(0, 3).join("/") + "/" + hash());
return true;
}
//add #bottom if needed
else if (hash() === "#bottom") {
location.hash = hash();
//No need to return here as changes in .hash will not navigate
}
//check if /# is present in URL. This is a pure cosmetical filter
if (location.pathname.substr(-1) !== "/") {
location.replace(location.href.substr(0, location.href.indexOf('#')) + "/" + hash());
return true;
}
return false;
};
//Removes the 4chan dereferrer and uses the "noreferrer" browser feature instead.
//This is faster and avoids tracking by 4chan
var fixLinks = function () {
var links = qa(".linkified");
for (var i = 0; i < links.length; i++) {
var l = links[i];
if (l.href.indexOf("://sys.4chan.org/derefer") > 0) {
l.href = decodeURI(decodeURIComponent(l.href.substr(l.href.indexOf("=") + 1)).replace(/&/g, "&"));
console.debug("Fixed link", l, l.href);
}
//Tip: Always specify "noopener" if the link leads to an external ressource.
l.setAttribute("rel", "noopener noreferrer");
}
};
//Toggles a custom style on or off
var toggleStyle = function (type, status) {
//List of supported style options
var styles = {
"download": "a[download]::after{content:\" \\1f4be \";}", //floppy disk
"arrow": "a[target=_blank]:not(.external-site)::before{content:\"\\2934 \";}", //arrow pointing rightwards then curving upwards
"pattern": ".fileThumb img{background-image:url(" + IMG_SRC + ")}",
"stats": ".thread-stats em{background-color:#F00;color:#FF0;font-weight:bold;}",
"general": "div.active-post{background-color:#FFF;color:#000;}",
"upsize": ".fileText{max-width:100% !important;}"
};
//For the unicode chars, check http://amp-what.com
var style = null;
if (typeof(styles[type]) === typeof("")) {
style = q("#fc_style_" + type);
if (status) {
style = style || ce("style", {
id: "fc_style_" + type
}, styles[type]);
}
if (style) {
if (status) {
console.debug("Adding style", type);
document.head.appendChild(style);
} else {
console.debug("Removing style", type);
style.remove();
}
} else {
console.error("Unable to find style or create an element");
}
} else {
console.error("Unsupported style toggle:", type);
}
return style;
};
//This removed the ugly Halloween CSS and returned to the default.
//We might need this again on next halloween
var fixCSS = function () {
//Easter 2018
var e = q("#no-easter") || ce("style", {
id: "no-easter"
}, "strong.capcode, .atsb2018,.party-hat {display:none;}");
document.head.appendChild(e);
//Forced style sheets
var c = q("link[media]");
if (c) {
c.remove();
}
//halloween skeleton
c = q("#skellington");
if (c) {
console.debug("Removed Halloween skeleton");
c.remove();
}
};
//This restores some settings.
var restoreSettings = function () {
console.debug("Restoring settings");
toggleStyle("download", GM_getValue("style_download", "y") === "y");
toggleStyle("arrow", GM_getValue("style_arrow", "y") === "y");
toggleStyle("pattern", GM_getValue("style_pattern", "y") === "y");
toggleStyle("stats", GM_getValue("style_stats", "y") === "y");
toggleStyle("upsize", GM_getValue("style_upsize", "y") === "y");
};
//Checks if the given string is to be treated as regular expression
var isRegex = function (x) {
return x.match(/^\/(.+)\/(\w?\w?)$/);
};
//Checks if the given string is to be treated as an MD5 hash
var isMd5 = function (x) {
return x.toLowerCase().indexOf('md5:') === 0;
};
//Checks if the given string matches the given match
var isMatch = function (str, match) {
var parts = isRegex(match);
if (parts) {
return !!str.match(new RegExp(parts[1], parts[2] || undefined));
}
return str.toLowerCase().indexOf(match) >= 0;
};
//This hides annoying poster by file name and text content match
var autoHide = function () {
//Faster than doing it on each iteration manually.
filenames = filenames.map(function (v) {
return (isRegex(v) || isMd5(v)) ? v : v.toLowerCase();
});
messages = messages.map(function (v) {
return isRegex(v) ? v : v.toLowerCase();
});
var links = qa("a[download]");
//scan for offending file names
for (var i = 0; i < filenames.length; i++) {
//for some reason you cannot use ".forEach(...)" on a NodeList
for (var j = 0; j < links.length; j++) {
var id = links[j].parentNode.id.substr(2);
if (!q("#p" + id).classList.contains(CLASS_PROCESSED)) {
if (isMatch(links[j].getAttribute("download"), filenames[i])) {
if (!ReplyHiding.isHidden(id)) {
console.debug("Automatically hiding Post", id, "Reason: Filename", filenames[i]);
ReplyHiding.hide(id);
q("#p" + id).classList.add(CLASS_PROCESSED);
}
}
}
}
}
//Scans for MD5 hashes
var thumbs = qa(".fileThumb img");
for (var k = 0; k < thumbs.length; k++) {
var md5 = thumbs[k].getAttribute('data-md5');
if (md5 && filenames.indexOf("MD5:" + md5) > -1) {
ReplyHiding.hide(thumbs[k].parentNode.parentNode.id.substr(1));
}
}
//scan for messages with offending content
for (var msgcount = 0; msgcount < messages.length; msgcount++) {
var ele = qa(".postMessage");
for (var entrycount = 0; entrycount < ele.length; entrycount++) {
var eid = ele[entrycount].id.substr(1);
if (!q("#p" + eid).classList.contains(CLASS_PROCESSED)) {
if (isMatch(ele[entrycount].textContent, messages[msgcount])) {
if (!ReplyHiding.isHidden(eid)) {
console.debug("Automatically hiding Post", eid, "Reason: Text", messages[msgcount]);
ReplyHiding.hide(eid);
q("#p" + eid).classList.add(CLASS_PROCESSED);
}
}
}
}
}
};
//Unhides all elements and removes the processed class
var unhideAll = function () {
console.debug("Unhiding all elements");
qa(".replyContainer").forEach(function (v) {
var id = v.id.substr(2);
if (ReplyHiding.isHidden(id)) {
q("#p" + id).classList.remove(CLASS_PROCESSED);
ReplyHiding.toggle(id);
}
});
};
//Sets the filter values
var setFilter = function (f, m) {
console.debug("Setting filter");
filenames = f.trim().split('\n').map(trim).filter(nonempty).filter(dedupe);
messages = m.trim().split('\n').map(trim).filter(nonempty).filter(dedupe);
};
//gets the comment box (the real one, not the template)
var getCom = function () {
return q("#quickReply textarea[name=com]");
};
//gets the post button (the real one, not the template)
var getBtn = function () {
return q("#quickReply [type=submit][value=Post]");
};
//gets the upload control (the real one, not the template)
var getUpBox = function () {
return q("#quickReply [name=upfile]");
};
//Handles [CTRL]+[ENTER]
var keyHandler = function (e) {
if (QUICKSEND && e.keyCode == 13 && e.ctrlKey) {
e.stopPropagation();
e.preventDefault();
var p = getBtn();
if (p) {
console.log("Sending Post because [CTRL]+[ENTER]");
updateLoop.timeout = REPLY_INTERVAL;
p.click();
}
}
};
//Enable File drag and drop
document.ondragover = function (e) {
var t;
if (e.dataTransfer && (t = e.dataTransfer.types)) {
//Only files
if (t.indexOf("Files") >= 0 || t.indexOf("application/x-moz-file") >= 0) {
e.dataTransfer.dropEffect = "move";
e.preventDefault();
}
}
};
//Show comment box and put file onto upload control by default
document.ondrop = function (e) {
var f = e.dataTransfer && e.dataTransfer.files;
if (f && f.length > 0) {
e.preventDefault();
if (f.length === 1) {
QR.show(Main.tid);
var upload = getUpBox();
if (upload) {
console.debug("Accepting File drop", f);
upload.files = f;
} else {
alert("Can't access file upload element");
}
var com = getCom();
if (com) {
com.select();
com.focus();
}
} else {
alert("You can only upload one file at a time.");
}
} else {
//No files. Probably text drop or something like that
}
};
//Trims whitespace from a string
var trim = function (x) {
return (x + "").trim();
};
//Deduplicate filter for arrays
var dedupe = function (v, i, a) {
return a.indexOf(v) === i;
};
//Non Empty string filter for arrays
var nonempty = function (v) {
return (v + "").length > 0;
};
//Gets absolute element offset
var getOffset = function (el) {
var box = el.getBoundingClientRect();
return {
left: box.left + window.scrollX,
top: box.top + window.scrollY,
width: box.width || el.clientWidth,
height: box.height || el.clientHeight
};
};
//Scrolls a post into view
var scrollIntoView = function (id) {
var e = q("#p" + id);
if (e) {
var pos = getOffset(e);
var centerPoint = pos.height / 2 | 0;
var windowCenter = window.innerHeight / 2 | 0;
window.scrollTo(window.scrollX, pos.top - windowCenter + centerPoint);
return pos;
} else {
console.warn("Reply id", id, "doesn't exists");
}
};
//Gets all post ids of the current thread (without the initial post)
var getIds = function () {
var ids = [];
var replies = qa(".reply");
for (var i = 0; i < replies.length; i++) {
ids[i] = +replies[i].id.substr(1);
}
return ids;
};
//Sets the active post id and highlights active post
var setActivePost = function (id) {
var node = q("#p" + id);
var posts = qa(".active-post");
for (var i = 0; i < posts.length; i++) {
posts[i].classList.remove("active-post");
}
if (node) {
node.classList.add("active-post");
setActivePost.id = id;
//Prevent navigation to that id because we want it in the middle
node.id += "_TEMP";
location.hash = "#p" + id;
node.id = "p" + id;
scrollIntoView(id);
} else {
setActivePost.id = null;
}
console.debug("Setting active post to", setActivePost.id);
};
setActivePost.id = null;
//Activates the next reply
var selectNextReply = function () {
var replies = getIds();
if (replies.length > 0) {
var pos = Math.min(replies.indexOf(+setActivePost.id) + 1, replies.length - 1);
setActivePost(replies[pos]);
}
};
//Activates the prevous reply
var selectPrevReply = function () {
var replies = getIds();
if (replies.length > 0) {
var pos = Math.max(replies.indexOf(+setActivePost.id) - 1, 0);
setActivePost(replies[pos]);
}
};
//Generates the settings dialog
var showDialog = function () {
console.debug("Opening Settings dialog");
var btnStyle = "font-size:12pt;font-family:sans-serif;width:50%;display:inline-block;margin-top:5px;padding:5px;";
var closeStyle = "font-size:12pt;font-family:sans-serif;float:right;padding:5px;";
var txtStyle = "width:100%;height:5em;display:block;margin-top:10px;margin-bottom:10px;font-family:" +
"Sans-Serif;font-size:10pt;color:#FF0000;background-color:#FFFFFF;font-weight:bold;";
if (showDialog.shown) {
return;
}
showDialog.shown = true;
//generate new random ID each time we do this
var randID = "configEle_" + (Math.random() * 1000000 | 0);
//create DIV element for user control
//We set many attributes to override conflicting defaults from the sites stylesheet
var div = ce("div", {
style: "all:unset;position:fixed;font-size:9pt;" +
"padding:10px;background-color:#FFFFFF;color:#000000;background-image:none;" +
"border:2px solid #000000;border-radius:5px;width:500px;height:auto;" +
"right:10px;top:10px;left:auto;bottom:auto;",
id: randID
});
//Title text generator
var title = function (x) {
//Div Title
var e = ce("span", {
style: "font-size:20pt;font-weight:bold;display:block;"
});
e.textContent = x;
return e;
};
//Div Title
var mainTitle;
div.appendChild(mainTitle = title("Content Filter"));
//Close button
var clb = ce("button", {
type: "button",
style: closeStyle
}, "X");
clb.addEventListener("click", function () {
showDialog.shown = false;
div.remove();
});
mainTitle.appendChild(clb);
//Help button
var hlpb = ce("button", {
type: "button",
style: closeStyle
}, "?");
hlpb.addEventListener("click", function (e) {
showDialog.shown = false;
div.remove();
ce("a", {
href: "https://cable.ayra.ch/4chan/help.php",
target: "_blank"
}, "Help").click();
});
mainTitle.appendChild(hlpb);
//Explanation
var info = ce("span", {}, "Use this to change the content filter. All entries are partial matches.<br />" +
"The filter will not automatically unhide elements, use the button below for that.");
div.appendChild(info);
div.appendChild(document.createElement("hr"));
//Blocked file names title
div.appendChild(document.createTextNode("Blocked File Names (one per line). If it starts and ends in '/', it's treated as regular expression. If it starts with 'MD5:', it's treated as image hash"));
div.appendChild(document.createElement("br"));
//Blocked file names filter
var txtFile = ce("textarea", {
style: txtStyle
});
txtFile.value = filenames.join('\n');
div.appendChild(txtFile);
//Blocked messages title
div.appendChild(document.createTextNode("Blocked Message content (one per line). If it starts and ends in '/', it's treated as regular expression."));
div.appendChild(document.createElement("br"));
//Blocked messages filter
var txtMessages = ce("textarea", {
style: txtStyle
});
txtMessages.value = messages.join('\n');
div.appendChild(txtMessages);
div.appendChild(title("Options"));
//Options
div.appendChild(document.createTextNode("Enable and disable options (saved immediately)."));
div.appendChild(document.createElement("br"));
//Checkbox with label and callback generator
var checkbox = function (text, description, state, callback) {
var box = document.createElement("div");
box.style.display = "inline-block";
box.style.padding = "2px";
var l = ce("label", {
"data-tip": description
});
var c = ce("input", {
type: "checkbox"
});
c.checked = !!state;
c.addEventListener("change", callback);
l.appendChild(c);
l.appendChild(document.createTextNode(" " + text));
box.appendChild(l);
return box;
};
//Download
div.appendChild(checkbox("Download Icons", "Shows a floppy disk icon on links that trigger a download", GM_getValue("style_download", "y") === "y", function () {
toggleStyle("download", this.checked);
GM_setValue("style_download", this.checked ? "y" : "n");
}));
//Pattern
div.appendChild(checkbox("Checkerboard pattern", "Puts a checkerboard pattern behind images to highlight transparency", GM_getValue("style_pattern", "y") === "y", function () {
toggleStyle("pattern", this.checked);
GM_setValue("style_pattern", this.checked ? "y" : "n");
}));
//Arrow
div.appendChild(checkbox("Arrow for new tab links", "Shows arrows for links that open in new tabs", GM_getValue("style_arrow", "y") === "y", function () {
toggleStyle("arrow", this.checked);
GM_setValue("style_arrow", this.checked ? "y" : "n");
}));
//Stats
div.appendChild(checkbox("Color stats", "Colors stats red that prevent a thread from being bumped", GM_getValue("style_stats", "y") === "y", function () {
toggleStyle("stats", this.checked);
GM_setValue("style_stats", this.checked ? "y" : "n");
}));
//Upsize
div.appendChild(checkbox("Wider posts", "Allows post boxes to be wider to not cut off long file links", GM_getValue("style_upsize", "y") === "y", function () {
toggleStyle("upsize", this.checked);
GM_setValue("style_upsize", this.checked ? "y" : "n");
}));
//Auto Retry
div.appendChild(checkbox("Automatically retry", "Retries posting on error", GM_getValue("autoretry", "n") === "y", function () {
GM_setValue("autoretry", this.checked ? "y" : "n");
}));
div.appendChild(document.createElement("br"));
//Reapply Button
var btnReapply = ce("input", {
type: "button",
value: "Unhide all and reapply filter",
style: btnStyle
});
btnReapply.addEventListener("click", function () {
setFilter(txtFile.value, txtMessages.value);
unhideAll();
autoHide();
});
div.appendChild(btnReapply);
//Save button
var btnSave = ce("input", {
type: "button",
value: "Save and Apply",
style: btnStyle
});
btnSave.addEventListener("click", function () {
console.debug("Saving Settings");
setFilter(txtFile.value, txtMessages.value);
GM_setValue("blacklist", JSON.stringify({
file: filenames,
msg: messages
}));
showDialog.shown = false;
div.remove();
autoHide();
}, false);
div.appendChild(btnSave);
div.appendChild(document.createElement("br"));
//Download warning
var dwnld = document.createElement("span");
if (GM_info.downloadMode !== "browser") {
dwnld.style.color = "#F00";
dwnld.innerHTML = "Downloads are not set up. Open your tampermonkey settings and " +
"(towards the bottom) change the download mode to 'Browser API', then reload this page. " +
"To get this setting, you need to enable expert mode.";
} else {
dwnld.style.color = "#090";
dwnld.innerHTML = "Downloads are set up properly.";
}
div.appendChild(dwnld);
div.appendChild(document.createElement("br"));
return document.body.appendChild(div);
};
//Resizes the reply box after it has been shown
var resizeReplyBox = function () {
var e = q("#qrEmail");
var err = q("#qrError");
var com = getCom();
var post = getBtn();
if (PASSUSER) {
var text = "since4pass";
if (e && e.value != text && e.getAttribute("pass") != "set") {
e.setAttribute("pass", "set");
e.value = text;
}
}
if (post && !post.getAttribute("data-hasevent")) {
post.addEventListener("click", function () {
updateLoop.timeout = REPLY_INTERVAL;
});
post.setAttribute("data-hasevent", "yes");
}
//make comment box larger
if (com && !com.getAttribute("data-hasevent")) {
console.debug("Resizing Reply Box");
com.style.width = "800px";
com.style.height = "200px";
com.addEventListener("keydown", keyHandler, false);
com.setAttribute("data-hasevent", "yes");
}
//Watch for errors on posting and automatically retry
if (err && !err.getAttribute("data-hasevent") && GM_getValue("autoretry", "y") === "y") {
err.setAttribute("data-hasevent", "yes");
var rmo = new MutationObserver(function () {
if (err && err.style.display == "block") {
var btn = q("#quickReply [type=submit]");
if (!btn.getAttribute("disabled")) {
if (btn) {
console.log("Automatic retry in 5 seconds");
btn.value = "Retry...";
btn.setAttribute("disabled", "disabled");
}
setTimeout(function () {
if (btn) {
btn.removeAttribute("disabled");
btn.click();
}
}, 5000);
}
}
});
rmo.observe(err, {
attributes: true
});
}
};
//Trigger updates after DOM change
var domTimer = function () {
fixLinks();
autoHide();
//always do this as last statement in the timer.
//Or at least after the last thing that updates the DOM
domUpdate.timer = null;
};
//DOM update handler
var domUpdate = function () {
//make sure to not excessively schedule timers
if (!domUpdate.timer) {
domUpdate.timer = window.setTimeout(domTimer, 500);
}
};
//Change Observer
var threadObserver = new MutationObserver(domUpdate);
var threadConfig = {
attributes: false,
childList: true,
characterData: false,
subtree: true
};
threadObserver.observe(q(".thread"), threadConfig);
//Rewrite Thread reply control
var replyFunc = QR.show.bind(QR);
QR.show = function (tid) {
var ret = replyFunc(tid);
resizeReplyBox();
return ret;
};
//Settings
var settingsLink = document.createElement("a");
settingsLink.href = "#";
settingsLink.style.fontWeight = "bold";
settingsLink.textContent = "4chan Utility Settings";
settingsLink.addEventListener("click", function (e) {
e.preventDefault();
showDialog();
});
//Show settings on first time
if (GM_getValue("settings_shown", "n") !== "y") {
console.debug("First Run");
GM_setValue("settings_shown", "y");
showDialog();
}
//Handler to select active reply
document.body.addEventListener("click", function (e) {
var node = e.target;
while (node && node.classList && !node.classList.contains("reply")) {
node = node.parentNode;
}
//setActivePost(node && node.classList ? node.id.substr(1) : null);
//Don't deselect
if (node && node.classList && node.id) {
setActivePost(node.id.substr(1));
}
});
//Handler for keys
document.addEventListener("keydown", function (e) {
//Only if reply and settings box are not visible
if (!showDialog.shown && !q("#qrEmail")) {
var stop = true;
switch (e.keyCode) {
case 72: //H
if (stop = !!setActivePost.id) {
ReplyHiding.toggle(setActivePost.id);
}
break;
case 40: //DOWN
selectNextReply();
break;
case 38: //UP
selectPrevReply();
break;
case 13: //Enter
var imgs = [
q("#p" + setActivePost.id + " .fileThumb img"),
q("#p" + setActivePost.id + " .fileThumb .expanded-thumb"),
];
if (imgs[1]) {
imgs[1].click();
} else if (imgs[0]) {
imgs[0].click();
}
break;
case 36: //Home
setActivePost(getIds()[0]);
break;
case 35: //End
setActivePost(getIds().slice(-1)[0]);
break;
case 45: //Insert
QR.show(Main.tid);
if (getCom()) {
getCom().focus();
}
break;
default:
stop = false;
break;
}
if (stop) {
e.preventDefault();
e.stopPropagation();
}
}
});
//enable global styles
toggleStyle("general", true);
//Add configuration link
q("#footer-links").appendChild(document.createTextNode(" • "));
q("#footer-links").appendChild(settingsLink);
//call everything once that does not calls itself upon modifications of the DOM
fixUrl();
updateFunction();
replaceTitle();
fixCSS();
restoreSettings();
//Restore eager image loading
qa("[loading]").forEach(function (v) {
v.loading = "eager";
});
//Select current post
if (location.hash) {
if (location.hash.match(/^#p\d+$/)) {
setActivePost(location.hash.substr(2));
}
if (location.hash == "#bottom") {
setActivePost(getIds().pop());
}
if (location.hash == "#top") {
setActivePost(getIds()[0]);
}
}
console.log("Completely initialized", GM_info.script.name, "Version:", GM_info.script.version);
})();
/*
LICENSE:
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
The full license text can be found here: http://creativecommons.org/licenses/by-nc-sa/4.0/
The link has an easy to understand version of the license and the full license text.
DISCLAIMER:
THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/
This script is marked as "external".
This means that the script was developed for someone else.
Because I don't use the script myself I will not know when it breaks.
If it breaks,
contact me for a fix.
User Script Managers
A userscript manager is the browser extension that injects scripts into websites
to change their behavior to your liking.
Recommendation
All scripts on this site have been developed and tested with Tampermonkey on firefox.
Try other browsers and other script managers at your own risk.
No script should use firefox or chrome specific features,
which means they should also work in other modern browsers.
If you prefer, you can use greasemonkey.
Get Tampermonkey,
Get Greasemonkey (Firefox only)
Script Installation
Once you have obtained a user script manager,
clicking on the install button will pop up an installation prompt.
To allow script manager detection,
you can install this helper script.
It's not necessary but simplifies your future visits to this site.
Script Installation
We detected, that you have a script manager installed and active.
Click the "Install Script" button to obtain the script.