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.48
// @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]
// - * 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.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;

    //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";

    //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 = 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);
            console.debug("Thread expired. Stopping updates");
            return;
        }
        if (box.checked && --counter <= 0) {
            counter = 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
        while (title.match(r)) {
            title = title.replace(r, "");
        }
        if (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);
                }
                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(/&amp;/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]::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;}"
        };

        //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");
    };

    //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]");
                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 limited stats", "Colors stats red that are limited", GM_getValue("style_stats", "y") === "y", function () {
                toggleStyle("stats", this.checked);
                GM_setValue("style_stats", this.checked ? "y" : "n");
            }));
        //Auto Retry
        div.appendChild(checkbox("Automatically retry", "Retries posting on error", GM_getValue("autoretry", "y") === "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();

        if (PASSUSER) {
            var text = "since4pass";
            if (e && e.value != text && e.getAttribute("pass") != "set") {
                e.setAttribute("pass", "set");
                e.value = text;
            }
        }

        //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.
*/

Copyright © 2018 by Kevin Gut 📧 | More services | Generated for 54.236.62.49