Empornium Deluxe Mode

Enhances the empornium.me porn torrent website

As of 2023-11-13. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Empornium Deluxe Mode
// @namespace    http://tampermonkey.net/
// @version      1.18
// @description  Enhances the empornium.me porn torrent website
// @author       codingjoe
// @match        https://*.empornium.me/*
// @match        https://*.empornium.sx/*
// @match        https://*.empornium.is/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

let layout = {
    "General": {
        "AlwaysHeader": {
            type: "checkbox",
            text: "Always keep header in view",
            defaultValue: false
        },
        "AutoDismiss": {
            type: "checkbox",
            text: "Auto-dismiss login timeout notification",
            defaultValue: false
        },
        "JumpToTop": {
            type: "checkbox",
            text: "Insert 'Jump to Top' link onto bottom-right corner of all pages",
            defaultValue: false
        },
        "PreventNewWindow": {
            type: "checkbox",
            text: "Prevent single-click links from opening a new window",
            defaultValue: false
        },
        "StripAnonym": {
            type: "checkbox",
            text: "Strip url anonymizers from links",
            defaultValue: false
        },
    },
    "Notifications": {
        "CopyClearBottomNotifs": {
            type: "checkbox",
            text: 'Move "clear" and "clear selected" links onto bottom of matched groups',
            defaultValue: false
        },
        "HideClearAll": {
            type: "checkbox",
            text: 'Hide "Clear" and "clear all" links to prevent accidental clearing',
            defaultValue: false
        },
        "AutoCheckNotif": {
            type: "checkbox",
            text: "When the link is clicked, check the torrent's checkbox for manual clearing",
            defaultValue: false
        },
    },
    "Torrent Page": {
        "AutoOpenFileList": {
            type: "checkbox",
            text: 'Auto-open filelist',
            defaultValue: false
        },
        "AutoOpenSpoilers": {
            type: "checkbox",
            text: 'Auto-open hidden text / spoilers',
            defaultValue: false
        },
        "AutoThankUploader": {
            type: "checkbox",
            text: 'Auto-thank the uploader upon clicking download / freeleech / doubleseed',
            defaultValue: false
        },
        "AutoLoadScaledImages": {
            type: "checkbox",
            text: 'Automatically load the full res of scaled-down images when possible',
            defaultValue: false
        }
    },
    "Torrent Listings": {
        "ShowImages": {
            type: "checkbox",
            text: 'Display hover images inline',
            defaultValue: false
        },
        "ShowHoverTip": {
            type: "checkbox",
            text: '    Display full hover tip inline',
            defaultValue: false,
            enabler: "ShowImages"
        },
        "HideSeeded": {
            type: "checkbox",
            text: 'Hide currently seeding torrents',
            defaultValue: false
        },
        "HideLeeching": {
            type: "checkbox",
            text: 'Hide currently leeching torrents',
            defaultValue: false
        },
        "HideGrabbed": {
            type: "checkbox",
            text: 'Hide previously grabbed torrents [incomplete download]',
            defaultValue: false
        },
        "HideSnatched": {
            type: "checkbox",
            text: 'Hide previously snatched torrents [finished]',
            defaultValue: false
        },
    },
    "Filters": {
        "FilterFilesize": {
            type: "checkbox",
            text: 'Only display torrents in the filesize range:',
            defaultValue: false
        },
        "FilesizeFilterRange": {
            type: "range",
            defaultValue: { lowerNumber: 0, lowerUnits: "KiB", higherNumber: 999, higherUnits: "TiB" },
            enabler: "FilterFilesize", // enabler indicates which checkbox must be checked to enable the field
            units: ["KiB", "MiB", "GiB", "TiB"]
        },
        "FilterTags": {
            type: "checkbox",
            text: 'Torrents displayed to me must <u>not</u> have any of the following tags (black list):',
            defaultValue: false
        },
        "TagListing": {
            type: "textarea",
            text: '(comma-separated list)',
            defaultValue: "bbc, big.black.cock, bbw, fat, obese, hairy, gay, trans, tranny, transsexual, scat, feces, poop, puke, vomit, censored",
            title: "black list",
            enabler: "FilterTags" // enabler indicates which checkbox must be checked to enable the field
        },
        "OnlyShowTheseTags": {
            type: "checkbox",
            text: 'Torrents displayed to me <b>must have</b> at least one of the following tags:',
            defaultValue: false
        },
        "OnlyShowTagListing": {
            type: "textarea",
            text: '(comma-separated list)',
            defaultValue: "swallowed.com, evilangel.com, amourangels.com, met-art.com, legalporno.com, mplstudios.com, femjoy.com, sexart.com, ftvgirls.com, teenpornstorage.com, domai.com, analteenangels.com, showybeauty.com, justteensite.com, facefucking.com, teenfidelity.com, chaturbate.com, myfreecams.com, twistys.com, atkgalleria.com, iameighteen.com",
            title: "must-have list",
            enabler: "OnlyShowTheseTags" // enabler indicates which checkbox must be checked to enable the field
        },
    },
    "Bonus": {
        "ShowRatioGoals": {
            type: "checkbox",
            text: 'Display ratio goals table on the Bonus Shop page (experimental)',
            defaultValue: false
        },
    }
};

let customContentStyle = `
    .emp_modal_content {
        display: inline !important;
        width: 900px !important;
        height: auto !important;
        background-color: #000525 !important;
        margin-top: 0% !important;
        margin-left: -450px !important;
        position: absolute !important;
        top: 0% !important;
        left: 50% !important;
        text-align: center !important;
        overflow-y: auto !important;
        color: #2C8466 !important;
    }
`;

let accordionStyle = {
    backgroundColor: "#0E2D4A",
    color: "white",
    cursor: "pointer",
    padding: "18px",
    width: "100%",
    border: "none",
    textAlign: "center",
    outline: "none",
    fontSize: "15px",
    transition: "0.4s",
    borderRadius: "5px"
};

let panelStyle = {
    padding: "0 18px",
    display: "none",
    overflow: "hidden",
    textAlign: "left",
    backgroundColor: "#000015",
    fontSize: "10pt"
};

let textareaStyle = {
    width: "870px",
    height: "65px",
    backgroundColor: "#0E2D4A",
    color: "silver",
    marginTop: "10px"
};

Element.prototype.props = function(json) {
    return Object.assign(this, json);
};

HTMLDocument.prototype.new = function(tagName) {
    return document.createElement(tagName);
};

Element.prototype.appendTo = function(element) {
    element.appendChild(this);
    return this;
};

Element.prototype.setStyle = function(styler) {
    Object.assign(this.style, styler);
    return this;
}

let GM_config = {};

class ConfigDialog {
    constructor(configId, headerText) {
        this.configId = configId;
        this.headerText = headerText;

        // create modal dialog background
        this.modal = document.body.appendChild(document.createElement("div")).setStyle({
            display: "none",
            position: "fixed",
            left: 0,
            top: 0,
            width: "100%",
            height: "100%",
            overflow: "auto",
            backgroundColor: "rgba(0,0,0,0.7)",
            zIndex: 9999
        });

        // create a space for content inside the modal
        this.content = this.modal.appendChild(Object.assign(document.createElement("div"), { className: "emp_modal_content" }));
        GM_addStyle(`
            .emp_modal_content {
                display: inline;
                width: 250;
                height: 250;
                background-color: white;
                margin-top: -125px;
                margin-left: -125px;
                position: absolute;
                top: 50%;
                left: 50%;
                text-align: center;
            }
        `);


        let self = this;
        // handle modal close on click
        window.addEventListener("click", function(e) {
            if (e.target === self.modal) {
                self.Close();
            }
        });

        // retrieve settings
        this.savedValues = GM_getValue("EmporniumEnhancementsConfig")? JSON.parse(GM_getValue("EmporniumEnhancementsConfig")): null;

        if (!this.savedValues) {
            // load default settings
            this.savedValues = this.GatherDefaultValues();
        }

        Object.keys(layout).forEach(header => {
            Object.keys(layout[header]).forEach(id => {
                if (id === "FilesizeFilterRange") {
                    if (!this.savedValues[id]) {
                        if (GM_getValue("fileSizeRange")) {
                            this.savedValues[id] = JSON.parse(GM_getValue("fileSizeRange"));
                        } else {
                            this.savedValues[id] = layout.Filters[id].defaultValue;
                        }
                    }
                } else {
                    // auto-detect new settings
                    if (!Object.keys(this.savedValues).includes(id)) {
                        let defaultValue = null;
                        Object.keys(layout).forEach(header => {
                            if (Object.keys(layout[header]).includes(id)) {
                                defaultValue = layout[header][id].defaultValue;
                            }
                        });

                        this.savedValues[id] = defaultValue;
                    }
                }
            });
        });
        GM_setValue("EmporniumEnhancementsConfig", JSON.stringify(this.savedValues));
        GM_config = this.savedValues;

        this.Customize_UI(self);
    }

    Open() {
        this.modal.style.display = "block";
        document.body.style.overflow = "hidden";
    }

    Close() {
        this.modal.style.display = "none";
        document.body.style.overflow = "";
        this.ResetUI();
    }

    ResetUI() {
        // collapse all accordion headers
        document.querySelectorAll(".configHeader").forEach(r => {
            if (r.classList.contains("active_header")) {
                r.classList.remove("active_header");
                r.style.backgroundColor = "#0E2D4A";
            }
        });

        // hide all panels with a list of settings
        document.querySelectorAll(".configPanel").forEach(r => {
            r.style.display = "none";
        });

        // reset fields to the stored values
        document.querySelectorAll(`[id^='${this.configId}_var_']`).forEach(r => {
            switch (r.type || r.getAttribute("type")) {
                case "checkbox":
                    r.checked = this.savedValues[r.id.replace(`${this.configId}_var_`,"")];

                    // determine disabled state of fields the checkbox governs
                    document.querySelectorAll(`[data-enabler='${r.id.replace(`${this.configId}_var_`, "")}']`).forEach(d => {
                        //d.disabled = !this.checked;
                        if (this.checked) {
                            d.disabled = true;
                        } else {
                            if (d.hasAttribute("disabled")) {
                                d.removeAttribute("disabled");
                            }
                        }
                    });
                    break;
                case "textarea":
                case "text":
                case "number":
                case "date":
                case "select":
                    r.value = this.savedValues[r.id.replace(`${this.configId}_var_`,"")];
                    break;
                case "range":
                    r.querySelectorAll("input,select").forEach(element => element.value = this.savedValues[r.id.replace(`${this.configId}_var_`,"")][element.id]);
                    break;
                default:
                    console.log(`ResetUI implement '${r.type || r.getAttribute("type")}'`);
            }
        });
    }

    GatherFieldValues() {
        let self = this;
        this.savedValues = {};

        // gather setting values from input fields
        document.querySelectorAll(`[id^='${this.configId}_var_']`).forEach(r => {
            switch (r.type || r.getAttribute("type")) {
                case "checkbox":
                    this.savedValues[r.id.replace(`${this.configId}_var_`,"")] = r.checked;
                    break;
                case "textarea":
                case "text":
                case "number":
                case "date":
                case "select":
                    this.savedValues[r.id.replace(`${this.configId}_var_`,"")] = r.value;
                    break;
                case "range":
                    let rangeId = r.id.replace(`${this.configId}_var_`,"");
                    this.savedValues[rangeId] = {};
                    r.querySelectorAll("input,select").forEach(element => this.savedValues[rangeId][element.id] = element.value);
                    break;
                default:
                    console.log(`GatherFieldValues implement '${r.type || r.getAttribute("type")}'`);
            }
        });
    }

    GatherDefaultValues() {
        let defaultValues = {};

        // gather a list of defaults from the layout
        Object.keys(layout).forEach(header => {
            Object.keys(layout[header]).forEach(id => {
                let setting = layout[header][id];
                defaultValues[id] = setting.defaultValue;
            });
        });

        return defaultValues;
    }

    ResetFieldsToDefaults() {
        let defaultValues = this.GatherDefaultValues();

        // reset fields to their default values
        document.querySelectorAll(`[id^='${this.configId}_var_']`).forEach(r => {
            switch (r.type || r.getAttribute("type")) {
                case "checkbox":
                    r.checked = defaultValues[r.id.replace(`${this.configId}_var_`,"")];
                    break;
                case "textarea":
                case "text":
                case "number":
                case "date":
                case "select":
                    r.value = defaultValues[r.id.replace(`${this.configId}_var_`,"")];
                    break;
                case "range":
                    let rangeId = r.id.replace(`${this.configId}_var_`,"");
                    let rangeValues = {};
                    Object.keys(layout).forEach(k => {
                        if (Object.keys(layout[k]).includes(rangeId)) {
                            rangeValues = layout[k][rangeId].defaultValue;
                        }
                    });

                    r.querySelectorAll("input,select").forEach(element => element.value = rangeValues[element.id]);
                    break;
                default:
                    console.log(`ResetFieldsToDefaults implement '${r.type || r.getAttribute("type")}'`);
            }
        });

        document.querySelectorAll("[data-enabler]").forEach(r => r.disabled = true);
    }

    Save() {
        this.GatherFieldValues();
        // store to userscript
        GM_setValue("EmporniumEnhancementsConfig", JSON.stringify(this.savedValues));
        GM_config = this.savedValues;
        this.Close();
        location.reload();
    }

    RenderFields(self, header) {
        // generate settings panel
        let panel = this.content.appendChild(document.createElement("div"));
        panel.classList.add("configPanel");
        panel.setStyle(panelStyle);

        Object.keys(layout[header]).forEach(id => {
            let setting = layout[header][id];
            let fieldContainer = document.createElement("div");
            let label = null;
            let hr = null;
            let enabler = null;

            if (setting.type !== "range") {
                label = fieldContainer.appendChild(Object.assign(document.createElement("label"), { innerHTML: setting.text }));
                label.setAttribute("for", `${this.configId}_var_${id}`);
                label.style.marginRight = "5px";
            }

            switch (setting.type) {
                case "checkbox":
                    let chk = fieldContainer.appendChild(Object.assign(document.createElement("input"), { id: `${this.configId}_var_${id}`, type: setting.type }));
                    chk.checked = this.savedValues[id];

                    if (setting.enabler !== undefined) {
                        chk.setAttribute("data-enabler", setting.enabler);
                        enabler = panel.querySelector(`#${this.configId}_var_${setting.enabler}`);
                        chk.disabled = !enabler.checked;
                    }

                    break;

                case "text":
                    let textbox = fieldContainer.appendChild(Object.assign(document.createElement("input"), { id: `${this.configId}_var_${id}`, type: setting.type, value: this.savedValues[id] }));
                    break;

                case "textarea":
                    let textarea = fieldContainer.appendChild(Object.assign(document.createElement("textarea"), { id: `${this.configId}_var_${id}`, value: this.savedValues[id] })).setStyle(textareaStyle);

                    hr = fieldContainer.appendChild(document.createElement("hr"));
                    hr.style = "margin-top: 5px; margin-bottom: 10px";

                    if (setting.enabler !== undefined) {
                        textarea.setAttribute("data-enabler", setting.enabler);
                        enabler = panel.querySelector(`#${this.configId}_var_${setting.enabler}`);
                        textarea.disabled = !enabler.checked;
                    }
                    break;

                case "range":
                    let rangeFields = [];
                    let lowerUnits = null;
                    let higherUnits = null;

                    let lowerNumber = fieldContainer.appendChild(document.createElement("input")).setStyle({ width: "50px" });
                    Object.assign(lowerNumber, {
                        id: "lowerNumber",
                        type: "number",
                        min: 0,
                        max: 999,
                        value: this.savedValues[id].lowerNumber
                    });
                    rangeFields.push(lowerNumber);

                    if (setting.units) {
                        lowerUnits = fieldContainer.appendChild(Object.assign(document.createElement("select"), { id: "lowerUnits" }));
                        rangeFields.push(lowerUnits);
                    }

                    fieldContainer.appendChild(Object.assign(document.createElement("label"), { innerText: " to " }));

                    let higherNumber = fieldContainer.appendChild(document.createElement("input")).setStyle({ width: "50px" });
                    Object.assign(higherNumber, {
                        id: "higherNumber",
                        type: "number",
                        min: 0,
                        max: 999,
                        value: this.savedValues[id].higherNumber
                    });
                    rangeFields.push(higherNumber);

                    if (setting.units) {
                        higherUnits = fieldContainer.appendChild(Object.assign(document.createElement("select"), { id: "higherUnits" }));
                        rangeFields.push(higherUnits);

                        setting.units.forEach(u => {
                            lowerUnits.appendChild(Object.assign(document.createElement("option"), { text: u, value: u }));
                            higherUnits.appendChild(Object.assign(document.createElement("option"), { text: u, value: u }));
                        });

                        lowerUnits.value = this.savedValues[id].lowerUnits;
                        higherUnits.value = this.savedValues[id].higherUnits;
                    }

                    rangeFields.forEach(r => r.setStyle({ backgroundColor: "#0E2D4A", color: "silver" }));

                    if (setting.enabler !== undefined) {
                        enabler = panel.querySelector(`#${this.configId}_var_${setting.enabler}`);

                        rangeFields.forEach(r => {
                            r.setAttribute("data-enabler", setting.enabler);
                            r.disabled = !enabler.checked;
                        });
                    }

                    hr = fieldContainer.appendChild(document.createElement("hr"));
                    hr.style = "margin-top: 5px; margin-bottom: 10px";

                    fieldContainer.id = `${this.configId}_var_${id}`;
                    fieldContainer.setAttribute("type", setting.type);
                    break;

                default:
                    fieldContainer = Object.assign(document.createElement("div"), { innerText: setting.text });
                    break;
            }

            // tie checkox to trigger the disabled state of the form elements it governs
            if (enabler) {
                enabler.addEventListener("change", function(e) {
                    document.querySelectorAll(`[data-enabler='${this.id.replace(self.configId + "_var_", "")}']`).forEach(r => r.disabled = !this.checked);
                });
            }

            panel.appendChild(fieldContainer);
        });
    }

    RenderAccordion(self) {
        Object.keys(layout).forEach(header => {
            // generate accordion
            let category = this.content.appendChild(Object.assign(document.createElement("button"), { innerText: header }));
            category.classList.add("configHeader");
            category.setStyle(accordionStyle);
            // activate accordion header on mouse enter
            category.addEventListener("mouseenter", function(e) {
                this.style.backgroundColor = "#133C5F";
            });
            category.addEventListener("mouseleave", function(e) {
                // deactivate accordion header on mouse leave
                if (!this.classList.contains("active_header")) {
                    this.style.backgroundColor = "#0E2D4A";
                }
            });
            category.addEventListener("click", function(e) {
                // toggle accordion active state
                if (this.classList.contains("active_header")) {
                    this.classList.remove("active_header");
                } else {
                    this.classList.add("active_header");
                }

                let settingsPanel = this.nextElementSibling;
                if (settingsPanel.style.display === "block") {
                    settingsPanel.style.display = "none";
                } else {
                    settingsPanel.style.display = "block";
                }
            });

            // render fields
            this.RenderFields(self, header);
        });
    }

    Customize_UI(self) {
        // customize content box
        GM_addStyle(customContentStyle)

        // create UI elements

        // header text
        let headerDiv = this.content.appendChild(document.createElement("div")).setStyle({ fontSize: "20pt" });
        let lblTitle = headerDiv.appendChild(Object.assign(document.createElement("label"), { innerHTML: this.headerText }));

        this.RenderAccordion(self);

        // footer to contain buttons on the right
        let footerDiv = this.content.appendChild(document.createElement("div")).setStyle({ width: "100%", textAlign: "right", marginTop: "10px" });

        // create save button
        let btnSave = footerDiv.appendChild(Object.assign(document.createElement("input"), { type: "button", value: "Save" })).setStyle({ width: "50px" });
        btnSave.addEventListener("click", function(e) {
            self.Save();
        });

        // create reset to defaults button
        let btnReset = footerDiv.appendChild(Object.assign(document.createElement("input"), { type: "button", value: "Reset to defaults" })).setStyle({ width: "115px" });
        btnReset.addEventListener("click", function(e) {
            self.ResetFieldsToDefaults();
        });
    }
}


function myRound(x, places) {
    let trunc = Math.pow(10, places);
	return Math.round(x * trunc) / trunc;
}

function Insert_JumpToTop() {
    if (GM_config.JumpToTop) {
        // create a div fixed in the bottom-right corner
        let jumpDiv = document.createElement("div");
        jumpDiv.setStyle({
            position: "fixed",
            bottom: "5px",
            right: "5px",
            textAlign: "right"
        });

        // create the 'Jump to Top' link
        let anchor = document.createElement("a");
        anchor.innerText = "Jump to Top";
        anchor.href = "javascript:window.scrollTo(0, 0)";

        // append the link to the corner div
        jumpDiv.appendChild(anchor);

        // append the entire element to the body of the page
        document.body.appendChild(jumpDiv);
    }
}

function Affix_Header() {
    if (GM_config.AlwaysHeader) {
        if (document.querySelectorAll("#header").length > 0) {
            // affix the header into position
            document.querySelector("#header").style.cssText = "position: fixed !important; top: 0px; left: 0px;";
            // shift the contents down to account for missing space
            document.querySelector("#content").style.cssText = "margin-top:120px;";
        }
    }
}

function Strip_Anon(links) {
    if (GM_config.StripAnonym) {
        // list of url anonymizers to remove
        let anonymizers = [ "http://anonym.to/?", "http://anon.now.im/?", "https://anonym.es/?", "http://anonym.es/?" ];

        // loop over each link
        links.forEach(link => {
            // loop thru the list of anonymizers
            anonymizers.forEach(anonymizer => {
                // if link contains current anonymizer
                if (link.href.indexOf(anonymizer) >= 0) {
                    console.log(`Removing url anonymizer '${anonymizer}' from link '${link.href}'`);
                    // replace it with empty string
                    link.href = link.href.replace(anonymizer, "");
                }
            });
        });
    }
}

function Prevent_NewWindow(links) {
    if (GM_config.PreventNewWindow) {
        // loop over each link
        links.forEach(link => {
            // if a new window target has been set for current link
            if (link.target.length > 0) {
                // remove its target
                link.target = '';
            }
        });
    }
}

function AutoOpen_Spoilers(links) {
    if (GM_config.AutoOpenSpoilers) {
        console.log("AutoOpenSpoilers");
        // find all spoiler links
        links.filter(r => r.innerHTML === "Show").forEach(link => {
            // click to unhide
            link.click();
        });
    }
}

function AutoDismiss_LoginTimeout() {
    if (GM_config.AutoDismiss) {
        if (document.querySelectorAll("#flashClose").length > 0) {
            document.querySelector("#flashClose").click()
        }
    }
}

function RedirectTo_LoginScreen(links) {
    if (links.filter(r => r.innerHTML === "Login").length > 0) {
        window.location.href += "login";
    }
}

function SetFocus_LoginForm() {
    document.querySelector("input[name=username]").focus();
}

function Show_RatioGoals() {
    if (GM_config.ShowRatioGoals) {
        var stats = document.querySelectorAll(".stat");
        var units = [ "KiB", "MiB", "GiB", "TiB" ];

        var ratio = parseFloat(stats[5].innerText);
        var down = stats[3].innerText;
        var up = stats[1].innerText;

        var upUnitsIdx = units.indexOf(up.substring(1 + up.indexOf(" ")));
        var downUnitsIdx = units.indexOf(down.substring(1 + down.indexOf(" ")));

        var unitsDiff = parseInt(Math.round(upUnitsIdx - downUnitsIdx));
        var displayUnits = units[downUnitsIdx];

        up = up.replace(/,/g, "");
        up = parseFloat(up.substring(0, up.indexOf(" ")));
        down = down.replace(/,/g, "");
        down = parseFloat(down.substring(0, down.indexOf(" ")));

        // convert totals to same units
        if (unitsDiff < 0) {
            down *= Math.pow(1024, -unitsDiff);
            displayUnits = units[upUnitsIdx];
        } else if (unitsDiff > 0) {
            up *= Math.pow(1024, unitsDiff);
        }

        var diff = down / 100.0;
        var currAmount = Math.abs((ratio + 0.01) * down - up);

        let strHtml = "<table style=\"text-align:center !important;margin: 0px auto; width:50%;\">\
                           <tbody>\
                               <tr>\
                                   <td style=\"width:90px;text-align:center;\">U/L differential</td>\
                                   <td style=\"width:90px;text-align:center;\"> for ratio </td>\
                                   <td style=\"width:90px;text-align:center;\"> Amount to U/L </td>\
                               </tr>";

        for (let i = 1; i <= 10; i++) {
            var currRatio = i/100.0 + ratio;

            // alternate background color
            var style = "";
            if (i % 2 != 0) {
                style = " style=\"background-color:#222222 !important;\"";
            }

            strHtml += "<tr" + style + "><td style=\"text-align:center;\"> +" + (i == 1 ? myRound(currAmount, 3) : myRound(diff, 3)) + " " + displayUnits + " </td><td style=\"text-align:center;\"> " + myRound(currRatio, 3) + " </td><td style=\"text-align:center;\"> " + myRound(currAmount, 3) + " " + displayUnits + " </td></tr>";

            currAmount += diff;
        }

        strHtml += "</tbody></table>";

        var content = document.querySelector("#content");
        content.innerHTML = strHtml + content.innerHTML;
    }
}

function Hide_Seeded(torrents) {
    if (GM_config.HideSeeded) {
        torrents.filter(r => r.querySelectorAll(".icon_disk_seed").length > 0).forEach(itemRow => { itemRow.style = "display:none"; });
    }
}

function Hide_Grabbed(torrents) {
    if (GM_config.HideGrabbed) {
        torrents.filter(r => r.querySelectorAll(".icon_disk_grabbed").length > 0).forEach(itemRow => { itemRow.style = "display:none"; });
    }
}

function Hide_Snatched(torrents) {
    if (GM_config.HideSnatched) {
        torrents.filter(r => r.querySelectorAll(".icon_disk_snatched").length > 0).forEach(itemRow => { itemRow.style = "display:none"; });
    }
}

function Hide_Leeching(torrents) {
    if (GM_config.HideLeeching) {
        torrents.filter(r => r.querySelectorAll(".icon_disk_leech").length > 0).forEach(itemRow => { itemRow.style = "display:none"; });
    }
}

function BlackList_TheseTags(torrents) {
    if (GM_config.FilterTags) {
        // grab the filters from the settings
        let filters = GM_config.TagListing.replace(/[\n\r\s]/g, "").split(',');
        let intersect = function(A,B) { return A.filter(r => B.indexOf(r) >= 0) }
        let intersects = function(A,B) { return intersect(A,B).length > 0 }

        // loop over each torrent
        torrents.filter(r => r.style.display !== "none").forEach(itemRow => {
            // grab the current torrent's list of tags
            let tags = itemRow.querySelector(".tags").innerText.split(" ");

            // if the filter keywords and tags have items in common
            if (intersects(filters,tags)) {
                // hide this torrent row
                itemRow.style.display = "none";
                let torrentInfo = Array.from(itemRow.querySelectorAll("a")).filter(r => /\/torrents\.php\?id\=\d+$/.test(r.href))[0]
                console.log(`Blacklisted found: "${intersect(filters,tags).join(",")}", therefore the torrent "${torrentInfo.href} - ${torrentInfo.innerText}" was hidden`);
            }
        });
    }
}

function Display_ImagesInline(torrents) {
    if (GM_config.ShowImages) {
        // loop over each torrent
        torrents.forEach(itemRow => {
            // if current torrent row contains a hover script and a cats_col class of element exists within it
            if (itemRow.querySelectorAll("script").length > 0 && itemRow.querySelectorAll("[class*=cats_col]").length > 0) {
                // extract the image-generating html from the hover script
                let strHtml = itemRow.querySelector("script").innerHTML.replace(/[\[\]\{\}\(\)\\\|]/g, "");
                let start = strHtml.indexOf("\"")+1;
                let end = strHtml.lastIndexOf("\"");
                strHtml = strHtml.substring(start, end);

                // create a div within which to place the image
                let div = document.createElement("div");

                // hide the cats_col's child elements which contain a definite title to make room for the image
                Array.from(itemRow.querySelector("[class*=cats_col]").childNodes).filter(r => r.title !== undefined).forEach(cat => { cat.style.display = "none"; });

                // add the html to generate the image to the div
                div.innerHTML = strHtml;

                // display the image in the torrent row
                if (GM_config.ShowHoverTip) {
                    itemRow.querySelector("[class*=cats_col]").appendChild(div);
                } else {
                    itemRow.querySelector("[class*=cats_col]").appendChild(div.querySelector("img"));
                }
            }
        });
    }
}

function Hide_ClearAll(links) {
    if (GM_config.HideClearAll) {
        // hide "clear all" and "Clear"
        links.filter(r => r.innerHTML === "(clear all)" || r.innerHTML === "Clear").forEach(link => { link.style.display = "none"; });
    }
}

function AutoCheck_ClickedTorrent(torrents) {
    if (GM_config.AutoCheckNotif) {
        // loop over entire torrent list
        torrents.forEach(itemRow => {
            // filter-out links on current torrent that do not contain "/torrents.php?id"
            Array.from(itemRow.querySelectorAll("a")).filter(r => r.href.indexOf("/torrents.php?id") > -1).forEach(link => {
                link.addEventListener("mousedown", function (e) {
                    // activate checkbox of current torrent if notification link was clicked
                    itemRow.querySelector("input[type=checkbox]").checked = true;
                });
            });
        });
    }
}

function Move_ClearToGroupBottom() {
    if (GM_config.CopyClearBottomNotifs) {
        // look for the torrent_table class of elements
        document.querySelectorAll(".torrent_table").forEach(t => {
            // find the current table's previous sibling's previous sibling
            let ps = t.previousSibling.previousSibling;
            // clone the element
            let c = ps.cloneNode(true);
            // insert at the bottom
            t.parentNode.insertBefore(c, t.nextSibling);
            ps.innerHTML = ps.innerHTML.toString().match(/(\w+\s){4}/gm);
        });
    }
}

function Filter_Filesizes(torrents) {
    if (GM_config.FilterFilesize) {
        let units = [ "KiB", "MiB", "GiB", "TiB" ];
        let filesizeRange = GM_config.FilesizeFilterRange;
        let torrentInfo = null;
        let colhead = document.querySelector(".colhead");
        let torrentHeader = null;
        if (colhead) {
            torrentHeader = Array.from(colhead.querySelectorAll("td")).map(r => r.innerHTML.startsWith("<img")? r.innerHTML.match(/title\=\"([^\"]+)\"/)[1]: r.innerText);

            // loop over each torrent
            torrents.filter(r => r.style.display !== "none").forEach(itemRow => {
                let sizeCell = itemRow.querySelectorAll("td")[torrentHeader.indexOf("Size")];

                // retrieve filesize of current torrent
                let currSize = sizeCell.innerText.split(" ");
                currSize[0] = currSize[0].replace(",","");

                // hide row if units of current row are less than units of min range
                if (units.indexOf(currSize[1]) < units.indexOf(filesizeRange.lowerUnits)) {
                    itemRow.style.display = "none";
                    torrentInfo = Array.from(itemRow.querySelectorAll("a")).filter(r => /\/torrents\.php\?id\=\d+$/.test(r.href))[0]
                    console.log(`${currSize.join("")} < ${filesizeRange.lowerNumber}${filesizeRange.lowerUnits}; hide the torrent '${torrentInfo.href} - ${torrentInfo.innerText}'`);
                } else if (currSize[1] === filesizeRange.lowerUnits) {
                    // hide row if units match and filesize is less than min range
                    if (parseFloat(currSize[0]) < parseInt(filesizeRange.lowerNumber)) {
                        itemRow.style.display = "none";
                        torrentInfo = Array.from(itemRow.querySelectorAll("a")).filter(r => /\/torrents\.php\?id\=\d+$/.test(r.href))[0]
                        console.log(`${currSize.join("")} < ${filesizeRange.lowerNumber}${filesizeRange.lowerUnits}; hide the torrent '${torrentInfo.href} - ${torrentInfo.innerText}'`);
                    }
                }

                // hide row if units of current row are greater than units of max range
                if (units.indexOf(currSize[1]) > units.indexOf(filesizeRange.higherUnits)) {
                    itemRow.style.display = "none";
                    torrentInfo = Array.from(itemRow.querySelectorAll("a")).filter(r => /\/torrents\.php\?id\=\d+$/.test(r.href))[0]
                    console.log(`${currSize.join("")} > ${filesizeRange.higherNumber}${filesizeRange.higherUnits}; hide the torrent '${torrentInfo.href} - ${torrentInfo.innerText}'`);
                } else if (currSize[1] === filesizeRange.higherUnits) {
                    // hide row if units match and filesize is greater than max range
                    if (parseFloat(currSize[0]) > parseInt(filesizeRange.higherNumber)) {
                        itemRow.style.display = "none";
                        torrentInfo = Array.from(itemRow.querySelectorAll("a")).filter(r => /\/torrents\.php\?id\=\d+$/.test(r.href))[0]
                        console.log(`${currSize.join("")} > ${filesizeRange.higherNumber}${filesizeRange.higherUnits}; hide the torrent '${torrentInfo.href} - ${torrentInfo.innerText}'`);
                    }
                }
            });
        } else {
            console.log("Unable to filter filesizes; could not find torrent listing column header.");
        }
    }
}

function Draw_MenuItem(callback) {
    if (document.querySelector(".username") != null) {
        // config icon - wrench & screwdriver depicted
        let icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA\
                    u0lEQVQ4jZ2SMQ4CMQwERyiICqEr+EGgRBelpkvLI3kDD+IBdDQUFKbJIcdyTqezlGazu7\
                    ZXBlM55+1ajFLyYS2mQanv4hFDCNP/rKNUoid+NAZql5MxFODriMtkEGPcNV2BsxIL8O6J\
                    vfVE7fd/w7C/LhFjOosxTEvETTnTjK7YO4hKDM40o+bZEO0qnwrZbNKSHF4OZoNtK6V4VC\
                    QtnkoAUYZuZwE2vRHVdbqXeO90ttgTuEEnRA+cw36EkV9UhABsAgAAAABJRU5ErkJggg==";
        // find the user nav element
        let navbar = document.querySelector(".username").parentNode.querySelector("ul");
        // clone a list item layout
        let configNode = navbar.querySelector("li").cloneNode(true);

        // wire-up the config element
        configNode.id = "deluxe_config";
        configNode.innerHTML = "";
        let anchor = document.new("a").appendTo(configNode);
        anchor.title = "Empornium Deluxe Mode Configuration";
        anchor.innerHTML = '<img src="' + icon + '" style="filter:invert(100%);"/> Deluxe Mode Config';
        anchor.href = "javascript:void(0)";
        anchor.addEventListener("click", function (e) {
            callback();
        });

        // display config link as first item in the user nav
        navbar.insertBefore(configNode, navbar.firstChild);
    }
}

function AutoThank_Uploader() {
    if (GM_config.AutoThankUploader) {
        // .blueButton => Download
        // .greenButton => Freeleech
        // .orangeButton => Doubleseed
        // look for the download / freeleech / doubleseed buttons
        Array.from(document.querySelectorAll(".blueButton,.greenButton,.orangeButton")).forEach(btn => {
            // wire-up event to auto-click thanks upon downloading
            btn.addEventListener("click", function (e) {
                window.setTimeout(function () {
                    // if thanks button not disabled
                    if (!document.querySelector("#thanksbutton").disabled) {
                        // invoke it
                        document.querySelector("#thanksbutton").click();
                    }
                    // wait 500ms so as not to interrupt download request
                }, 500);
            });
        });
    }
}

function AutoOpen_FileList(links) {
    if (GM_config.AutoOpenFileList) {
        links.filter(r => r.innerHTML === "(View Filelist)")[0].click();
    }
}

function MustHave_TheseTags(torrents) {
    if (GM_config.OnlyShowTheseTags) {
        // grab the filters from the settings
        let filters = GM_config.OnlyShowTagListing.replace(/[\n\r\s]/g, "").split(',');
        let intersect = function(A,B) { return A.filter(r => B.indexOf(r) >= 0) }
        let intersects = function(A,B) { return intersect(A,B).length > 0 }

        // loop over each torrent
        torrents.filter(r => r.style.display !== "none").forEach(itemRow => {
            // grab the current torrent's list of tags
            let tags = itemRow.querySelector(".tags").innerText.split(" ");

            // if the filter keywords and tags do not have items in common
            if (!intersects(filters,tags)) {
                // hide this torrent row
                itemRow.style = "display:none";
                let torrentInfo = Array.from(itemRow.querySelectorAll("a")).filter(r => /\/torrents\.php\?id\=\d+$/.test(r.href))[0]
                console.log(`Not found on must-have list: "${tags.filter(r => r !== "").join(",")}", therefore the torrent "${torrentInfo.href} - ${torrentInfo.innerText}" was hidden`);
            }
        });
    }
}

function AutoLoad_ScaledImages() {
    if (GM_config.AutoLoadScaledImages) {
        Array.from(document.querySelectorAll(".scale_image")).forEach((r, idx) => {
            setTimeout(function() {
                // detect thumbnails and medium scaled images
                let newsrc = r.src.replace(".th","").replace(".md","");
                r.src = newsrc;
                r.parentNode.href = newsrc;
            }, idx * 3000); // put a delay of 3sec between each full-res image request
        });
    }
}



// main
(function() {
    'use strict';

    let headerText = "Empornium Deluxe Mode Config";

    let config = new ConfigDialog("EmporniumConfig", headerText);

    GM_registerMenuCommand(headerText, function() {
        config.Open();
    });

    AutoDismiss_LoginTimeout();
    Draw_MenuItem(function() {
        config.Open();
    });

    let links = Array.from(document.querySelectorAll("a"));
    let torrents = Array.from(document.querySelectorAll(".torrent"));

    // page match rules
    if (/empornium\.(me|is|sx)\/?$/.test(window.location.href)) {
        RedirectTo_LoginScreen(links);
    } else if (/empornium\.(me|is|sx)\/login$/.test(window.location.href)) {
        SetFocus_LoginForm();
    } else {
        // occurs on all authenticated pages
        Affix_Header();
        Strip_Anon(links);
        Prevent_NewWindow(links);
        Insert_JumpToTop();


        if (/torrents\.php.+action=notify/.test(window.location.href)) {
            // notifications page
            Hide_ClearAll(links);
            Move_ClearToGroupBottom();
            AutoCheck_ClickedTorrent(torrents);
        }

        if (/(top10|user)\.php/.test(window.location.href) || (/torrents\.php/.test(window.location.href) && !/(\?|&)id=/.test(window.location.href))) {
            // torrents / top10 / user - lists of torrents
            BlackList_TheseTags(torrents);
            Filter_Filesizes(torrents);
            MustHave_TheseTags(torrents);

            Hide_Seeded(torrents);
            Hide_Grabbed(torrents);
            Hide_Snatched(torrents);
            Hide_Leeching(torrents);

            Display_ImagesInline(torrents);
        } else if (/torrents\.php\?id=\d+/.test(window.location.href)) {
            console.log("single torrent page");
            // single torrent landing page
            AutoOpen_Spoilers(links);
            AutoOpen_FileList(links);
            AutoThank_Uploader();
            AutoLoad_ScaledImages();
        } else if (/bonus\.php/.test(window.location.href)) {
            // bonus page
            Show_RatioGoals();
        }
    }
})();