KoLE2-alpha

Misc enhancements for kingdom of loathing

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        KoLE2-alpha
// @namespace   fnoot/kol/kole2-alpha
// @description Misc enhancements for kingdom of loathing
// @include     http://www.kingdomofloathing.com/*
// @include     https://www.kingdomofloathing.com/*
// @include     https://kingdomofloathing.com/*
// @version     0.2.4.1
// @grant    	GM_getValue
// @grant    	GM_setValue
// @grant    	GM_deleteValue
// ==/UserScript==
/*

changes 0.2.3 > 0.2.4
- fixed HP detection (for min autofight hp) in compact mode char pane
- 'auto button' option; choose choice adventure buttons to auto-click
- (.4.1) Fixed an incorrect title

changes 0.2.2 > 0.2.3
- 'Watch text' option; halt and show alert when specified text appears
- increased daily reminder click delay to account for unpredictable time before the buttons work
- improved example topbar theme
- fixed sword nullification (thanks to Marge for the Talkie for testing)
- improved styling here and there

changes 0.2.1 > 0.2.2
- quick "Auto Fight" checkbox
- auto daily reminder clicking

changes 0.2 > 0.2.1
- includes new https url

changes 0.1 > 0.2
- Topbar icon theming
- Auto fight min HP% setting
- /qs * <item> to sell all

*/
// kole 1 (defunct): http://userscripts.org/scripts/show/149194

(function() {
    var top = unsafeWindow.top; // :(

    //  debug
    //	GM_deleteValue("kole2Settings");
    //	GM_deleteValue("iconThemes");

    var defaultIconThemes = {
        example: {
            "displayName": "Example Theme (0.2.3)",
            "bg": "#e6e6e6",
            "icons": {
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/map.gif": "http://files.softicons.com/download/application-icons/pixelophilia-2-icons-by-omercetin/png/32/map.png",
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/backpack.gif": "http://findicons.com/files/icons/1334/take_a_hike/32/backpack.png",
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/donate.gif": "http://www.myiconfinder.com/uploads/iconsets/32-32-eea92a656253619edb72bf32fbe14ef9-bag.png",
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/book3.gif": "http://vignette1.wikia.nocookie.net/tibia/images/9/96/Spellbook_of_Dark_Mysteries.gif/revision/latest?cb=20080620235058&path-prefix=en",
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/pliers.gif": "https://cdn4.iconfinder.com/data/icons/harwdware-tools-v2/512/water_pump_pliers_tool-32.png",
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/envelope.gif": "http://files.softicons.com/download/toolbar-icons/iconza-light-blue-icons-by-turbomilk/png/32/mail.png",
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/blackwrench.gif": "http://findicons.com/files/icons/2262/android_developer_common_icon_set_ii/32/options_selected.png",
                "https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/chat.gif": "http://findicons.com/files/icons/1620/crystal_project/32/delete_group.png"
            }
        }
    };

    var saveIconThemes = function() {
        GM_setValue("iconThemes", JSON.stringify(top.kole.iconThemes));
    };
    
    var setOption = function(name, value) {
        GM_setValue(name, JSON.stringify(value));
    };

    var getOption = function(name) {
        var v = GM_getValue(name, JSON.stringify(configOptions[name].default));
        return JSON.parse(v);
    };

    var closePanel = null;

    var Notification = window.Notification || window.mozNotification || window.webkitNotification;
    var desktopNotify = function desktopNotify(title, options, properties) {
        if (!Notification) return;
        if (Notification.permission !== "granted") return;
        var notification = new Notification(title, options);
        if (properties) for (var k in properties) if (properties.hasOwnProperty(k)) notification[k] = properties[k];
    };


    
    var controlTypes = {
        input: function(name, spec, parent) {
            var input = crel("input", spec.style ? spec.style : {}, parent);
            input.id = "kole_config_" + name;
            var setValue = function() {
                if (spec.onchange) spec.onchange(input.value);
                setOption(name, input.value);
            };
            input.value = getOption(name);
            input.onkeyup = setValue;
            input.onpaste = setValue;
        },
        yesno: function(name, spec, parent) {
            var select = crel("select", {}, parent);
            select.id = "kole_config_" + name;
            with(crel("option", {}, select)) {
                innerHTML = "Yes";
                value = 1;
            }
            with(crel("option", {}, select)) {
                innerHTML = "No";
                value = 0;
            }
            select.selectedIndex = getOption(name) ? 0 : 1;
            select.onchange = function() {
                setOption(name, this.selectedIndex == 0);
            };
        },
        check: function(name, spec, parent) {
            var cbox = crel("input", {}, parent);
            cbox.checked = getOption(name);
            cbox.id = "kole_config_" + name;
            cbox.type = "checkbox";
            cbox.onclick = function() {
                setOption(name, this.checked);
                if (spec.onchange) spec.onchange(this.checked);
            }
        },
        spin: function(name, spec, parent) {
            var min = spec.range[0],
                max = spec.range[1];
            var value = getOption(name);
            if (isNaN(value)) value = configOptions[name].default;
            var displayCallback = spec.displayCallback || function(v) {
                return v;
            };
            var downButton = crel("button", {}, parent);
            downButton.innerHTML = "&lt;";
            var valueSpan = crel("span", {
                display: "inline-block",
                "margin": "0 6px",
                "width": "50px",
                "text-align": "center"
            }, parent);
            var fixPrecision = function() {
                value = Math.round(value * 100) / 100;
            }
            valueSpan.innerHTML = displayCallback(value);
            var upButton = crel("button", {}, parent);
            upButton.innerHTML = "&gt;";
            downButton.onclick = function() {
                value -= spec.step;
                fixPrecision();
                if (value < min) value = min;
                valueSpan.innerHTML = displayCallback(value);
                setOption(name, value);
                if (spec.onchange) spec.onchange(value);
            };
            upButton.onclick = function() {
                value += spec.step;
                fixPrecision();
                if (value > max) value = max;
                valueSpan.innerHTML = displayCallback(value);
                setOption(name, value);
                if (spec.onchange) spec.onchange(value);
            };
        },
        button: function(name, spec, parent) {
            with(crel("a", {}, parent)) {
                onclick = function() {
                    spec.onclick();
                    return false;
                };
                href = "#" + name;
                innerHTML = spec.buttonCaption;
            }
        },
        select: function(name, spec, parent) {
            var ctrl = crel("select", {}, parent);
            var options = typeof spec.options == "function" ? spec.options() : spec.options;
            var idx = 0;
            var selIndex = 0;
            for (var value in options) {
                var opt = crel("option", {}, ctrl);
                opt.innerHTML = options[value];
                opt.value = value;
                if (value == getOption("iconTheme")) selIndex = idx;
                idx++;
            }
            ctrl.selectedIndex = selIndex;
            ctrl.onchange = function() {
                setOption(name, ctrl.options[ctrl.selectedIndex].value);
                if (spec.onchange) spec.onchange(value);
            };
        },
        raw: function(name, spec, parent) {
            parent.innerHTML = spec.html;
        }
    };

    var getSetting = function(name) {
        console.trace();
        throw new Error("Obsolete getSetting()");
    };

    var itemLookup = function(fuzzyName) {
        if (!top.kole) return null;
        var matches = [];
        var item = null;
        for (var name in top.kole.itemIds) {
            if (name.trim().toUpperCase().indexOf(fuzzyName.toUpperCase()) > -1) {
                if (name.toLowerCase() == fuzzyName.toLowerCase()) {
                    return {
                        name: name,
                        id: top.kole.itemIds[name]
                    };
                }
                matches.push({
                    name: name,
                    id: top.kole.itemIds[name]
                });
            }
        }
        if (matches.length > 1) {
            return {
                error: "Multiple matches for \"" + fuzzyName + "\". Please be more specific."
            };
        } else if (matches.length == 1) {
            return matches[0];
        }
        return null;
    };

    var configOptions = {
        hoverHints: {
            default: true,
            control: "check",
            caption: "Hover hints",
            description: "Shows information about an item/effect/icon when the mouse pointer hovers over it"
        },
        hintDelay: {
            default: 225,
            caption: "Hint delay (milliseconds)",
            description: "Specifies how long you need to hover over an item/effect/icon before its description is shown",
            control: "spin",
            range: [0, 2000],
            step: 25
        },
        darkness: {
            default: 0.2,
            caption: "Darkness",
            description: "Darkens the whole game; great for headaches and photosensitives!",
            control: "spin",
            range: [0, 0.8],
            step: 0.1,
            onchange: function(value) {
                top.kole.setDarkness(value);
            },
            displayCallback: function(value) {
                return ((value / 0.8) * 100).toFixed(1).replace(/(\d+)\.0+$/, '$1') + "%";
            }
        },
        stayLoggedIn: {
            default: true,
            caption: "Stay logged in",
            description: "Defeats the timeout that logs you out after an idle period by sending a dummy request every two minutes",
            control: "check",
            onchange: function(value) {
                if (value) xhr("main.php", function() {}, false);
            }
        },
        nullifySword: {
            default: true,
            caption: "Fix prepositions",
            description: "Undoes the effect of the Sword - may no longer work",
            control: "check"
        },
        autoDaily: {
            default: false,
            caption: "Automatic daily reminders",
            description: "Automatically clicks the daily reminder buttons",
            control: "check"
        },
        iconTheme: {
            default: "",
            caption: "Topbar icon theme",
            control: "select",
            description: "Replaces the icons on the topbar with a custom theme",
            options: function() {
                var options = {
                    "": "Vanilla"
                };
                for (themeName in unsafeWindow.top.kole.iconThemes) {
                    options[themeName] = unsafeWindow.top.kole.iconThemes[themeName].displayName
                }
                return options;
            }
        },
        manageIconThemes: {
            caption: "Manage icon themes",
            description: "Import, delete and edit topbar icon themes",
            control: "button",
            buttonCaption: "Manage",
            onclick: function() {
                var reshow = function() {
                    var pop = crel("div");
                    var vanillaManage = crel("div");
                    var newThemeButton = crel("a", {}, vanillaManage);
                    crel("span", {}, vanillaManage).innerHTML = " ";
                    newThemeButton.innerHTML = "New theme";
                    newThemeButton.href = "#";
                    newThemeButton.onclick = function() {
                        managePoop.close();
                        editIconTheme("", reshow);
                    };
                    var importButton = crel("a", {}, vanillaManage);
                    crel("span", {}, vanillaManage).innerHTML = " ";
                    importButton.innerHTML = "Import";
                    importButton.href = "#";
                    importButton.onclick = function() {
                        managePoop.close();
                        importIconTheme(reshow);
                    };
                    crel("h1", {
                        margin: "0 0 12px 0",
                        "font-weight": "100"
                    }, pop).innerHTML = "Topbar Icon Themes";
                    var themeList = [
                        ["Vanilla", vanillaManage]
                    ];
                    for (var name in unsafeWindow.top.kole.iconThemes) {
                        var manage = crel("div");
                        var editButton = crel("a", {}, manage);
                        crel("span", {}, manage).innerHTML = " ";
                        editButton.innerHTML = "Edit";
                        editButton.href = "#";
                        var deleteButton = crel("a", {}, manage);
                        crel("span", {}, manage).innerHTML = " ";
                        deleteButton.innerHTML = "Delete";
                        deleteButton.href = "#";
                        var exportButton = crel("a", {}, manage);
                        crel("span", {}, manage).innerHTML = " ";
                        exportButton.innerHTML = "Export";
                        exportButton.href = "#";
                        with({
                            name: name,
                            theme: unsafeWindow.top.kole.iconThemes[name]
                        }) {
                            deleteButton.onclick = function() {
                                if (!confirm("Delete this theme?")) return;
                                delete top.kole.iconThemes[name];
                                if (name == getOption("iconTheme")) {
                                    setOption("iconTheme", "");
                                    closePanel();
                                    openPanel();
                                    saveSettings();
                                }
                                saveIconThemes();
                                managePoop.close();
                                reshow();
                                return false;
                            };
                            editButton.onclick = function() {
                                editIconTheme(name, reshow);
                                managePoop.close();
                                return false;
                            };
                            exportButton.onclick = function() {
                                poop("<h1 style='font-weight:100; margin:0 0 12px 0'>Export code for " + theme.displayName + "</h1>" + "<textarea onclick='this.select()' style='width:100%; background:rgba(0,0,0,0.2); height:200px' readonly='readonly'>" + exportIconTheme(name).replace(/\</g, "&lt;").replace(/\>/g, "&gt;") + "</textarea><p>Copy and share the above code to distribute your theme.</p>");
                                return false;
                            };
                        }

                        themeList.push([top.kole.iconThemes[name].displayName, manage]);
                    }
                    pop.appendChild(tabulate(themeList, "rgba(0,0,0,0.06"));
                    var managePoop = poop(pop);
                }; // /reshow
                reshow();
            }
        },
        autoFight: {
            default: false,
            caption: "Automatic fighting",
            description: "Always clicks the last item in the combat bar or \"Adventure again\" when available; requires combat bar enabled in KoL options",
            control: "check",
            onchange: function(val){
                quickAF.checked = val;
            }
        },
        autoFightDelay: {
            default: 4000,
            caption: "Automatic fighting delay",
            description: "Defines how long to wait before taking an automatic action in a fight",
            control: "spin",
            range: [500, 10000],
            step: 500,
            displayCallback: function(v) {
                return (v / 1000) + "sec";
            }
        },
        autoFightMinHp: {
            default: 40,
            caption: "Auto fighting minimum HP",
            description: "Cancels automatic fighting when HP drops below this",
            control: "spin",
            range: [0, 100],
            step: 5,
            displayCallback: function(v) {
                return v + "%";
            }
        },
        autoButton: {
            caption: "Automatic Buttons",
            description: "Choose which buttons to auto-click in choice adventures",
            control: "button",
            buttonCaption: "Manage",
            onclick: function(){
                var pop = crel("div");
                crel("h1", {
                    margin: "0 0 12px 0",
                    "font-weight": "100"
                }, pop).innerHTML = "Automatic Buttons";
                crel("p", {
                    "font-size": "small"
                }, pop).innerHTML = "Buttons listed below will be automatically clicked when they appear. Enter one button caption per line. Button captions are case-sensitive.";
                var buttonsStr = GM_getValue("autoButtons")
                if(typeof buttonsStr == "undefined") buttonsStr = "";
                var ta = crel("textarea", {
                    width: "100%",
                    height: "24vh"
                }, pop);
                ta.value = buttonsStr;
                var bottom = crel("div", {"text-align": "right"}, pop);
                var ok = crel("button", {}, bottom);
                ok.innerHTML = "OK";
                var popup = poop(pop);
                ok.onclick = function(){
                    GM_setValue("autoButtons", ta.value);
                    popup.close();
                };
            }            
        },
        autoButtonDelay: {
            default: 4000,
            caption: "Automatic button delay",
            description: "Defines how long to wait before automatically clicking a choice adventure button",
            control: "spin",
            range: [500, 10000],
            step: 500,
            displayCallback: function(v) {
                return (v / 1000) + "sec";
            }
        },
        watchText: {
            default: "",
            caption: "Watch text",
            description: "Watch for text on the page; shows notification and stops autofight. Separate strings with |",
            control: "input",
            style: {width: "95%"},
            onchange: function(val){
                if(val && Notification) Notification.requestPermission();
            }
        },
        chatHelp: {
            caption: "Chat commands",
            buttonCaption: "Show",
            "description": "Shows a list of chat commands added by KoLE",
            "control": "button",
            onclick: function() {
                var pop = crel("div");
                crel("h1", {
                    margin: "0 0 12px 0",
                    "font-weight": "100"
                }, pop).innerHTML = "KoLE Chat Commands";
                crel("p", {
                    "font-size": "small"
                }, pop).innerHTML = "These commands require <i>Modern</i> chat version selected in <a href='account.php?tab=chat'>KoL options</a>";
                pop.appendChild(tabulate([
                    ["<code>/wiki &lt;searchterm&gt;</code>", "Search Coldfront KoL wiki"],
                    ["<code>/qs [amount] &lt;itemname&gt;</code>", "Quicksell item; default amount is 1"],
                    ["<code>/qs * &lt;itemname&gt;</code>", "Quicksell all of an item"]
                ], "rgba(0,0,0,0.06"));
                poop(pop);
            }
        },
        donate: {
            caption: "Appreciation",
            control: "raw",
            description: "Has this been useful?",
            html: '<form target="_blank" action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top" style="margin:none;padding:none;display:inline">' +
                '<input type="hidden" name="cmd" value="_s-xclick">' +
                '<input type="hidden" name="hosted_button_id" value="G33Q3HVDX4G3Y">' +
                '<input type="submit" value="Show via PayPal" src="https://www.paypalobjects.com/en_GB/i/btn/btn_donate_SM.gif" border="0" name="submit">' +
                '<img alt="" border="0" src="https://www.paypalobjects.com/en_GB/i/scr/pixel.gif" width="1" height="1">' +
                '</form>'
        }

    };




    var prepositions = ["about", "above", "across", "after", "against", "along", "among", "around", "at", "before",
        "behind", "below", "beneath", "beside", "between", "beyond", "by", "down", "during", "except",
        "for", "from", "in", "inside", "into", "like", "near", "of", "off", "on", "onto", "out", "outside",
        "over", "past", "through", "throughout", "to", "under", "up", "upon", "with", "within", "without"
    ];

    var OLDnullifySword = function(s) {
        for (var i = 0; i < prepositions.length; i++) {
            var prep = prepositions[i];
            var search = new RegExp(" " + prep, "g");
            s = s.replace(search, "\x09" + prep);
            var search = new RegExp(prep + " ", "g");
            s = s.replace(search, prep + "\x09");
        }
        return s;
    };
    
    var nullifySword = function(s) {
        var sneakySpace = "\x09";
        for (var i = 0; i < prepositions.length; i++) {
            var prep = prepositions[i];
            var search = new RegExp(" " + prep, "g");
            s = s.replace(search, sneakySpace + prep);
            search = new RegExp(prep + " ", "g");
            s = s.replace(search, prep + sneakySpace + " ");
        }
        return s;
    };

    var tabulate = function(data, altColour, cellCallback) {
        var table = crel("table", {
            width: "100%",
            "font-size": "inherit"
        });
        table.setAttribute("cellspacing", 0);
        table.setAttribute("cellpadding", 4);
        if (!cellCallback) cellCallback = function(v) {
            return v;
        };
        var tb = crel("tbody", {}, table);
        if (data.length == 0) return table;
        var alt = false;
        for (var i = 0; i < data.length; i++) {
            var row = data[i];
            var tr = crel("tr", {}, tb);
            if (alt) tr.style.background = altColour;
            alt = !alt;
            for (var x = 0; x < row.length; x++) {
                var td = crel("td", {}, tr);
                var cbResult = cellCallback(row[x]);
                if (typeof cbResult == "string") {
                    td.innerHTML = cbResult;
                } else {
                    td.appendChild(cbResult);
                }
            }
        }
        return table;
    };

    var styleClassIdx = 0;
    var applyStyles = function(el, styles, pseudo){
        var style = document.createElement("style");
        if(typeof pseudo == "undefined") pseudo = "";
        if(pseudo != '') pseudo = ":" + pseudo;
        var className = "hover_styles";
        while(document.querySelectorAll("."+className).length > 0){
           className = "hover_style_" + styleClassIdx;
           styleClassIdx++;
        }
        var styleBlock = "";
        var keys = Object.keys(styles);
        for(var i = 0; i < keys.length; i++){ var key = keys[i];
            styleBlock += key + ":" + styles[key] + ";";
        }
        style.appendChild(document.createTextNode("." + className + pseudo + "{" + styleBlock + "}"));
        document.body.appendChild(style);
        el.classList.add(className);
    };

    // document.createElement > applyStyles > parent.append shortcut
    var crel = function(tag, styles, parent) {
        var el = document.createElement(tag);
        if (typeof styles != "undefined") applyStyles(el, styles);
        if (parent === null) {
            console.warn("parent is null for", tag);
            console.trace();
            return;
        }
        if (typeof parent != "undefined") parent.appendChild(el);
        return el;
    };

    var elX = function(el) {
        return el.offsetParent ? elX(el.offsetParent) + el.offsetLeft : el.offsetLeft;
    };
    var elY = function(el) {
        return el.offsetParent ? elY(el.offsetParent) + el.offsetTop : el.offsetTop;
    };


    if (window !== top) {
        var darkCover = crel("div", {
            "position": "fixed",
            "top": "0",
            "left": "0",
            "right": "0",
            "bottom": "0",
            "background": "#000",
            "opacity": 0,
            "pointer-events": "none",
            "z-index": 999999,
        }, document.body);
        var hintBox = crel("div", {
            "position": "absolute",
            "width": "260px",
            "padding": "12px 0px",
            "border": "1px #bbb solid",
            "border-radius": "5px",
            "box-shadow": "2px 2px 3px rgba(0,0,0,0.4)",
            "pointer-events": "none",
            "background": "#fff",
            "opacity": 0,
            "transform": "scale(0.1,0.1)",
            "transition": "100ms opacity ease-out, 100ms transform ease-out, 100ms -moz-transform ease-out, 100ms -webkit-transform ease-out",
            "z-index": 999998,
            "font-size": "small"
        }, document.body);
        setTimeout(function() {
            darkCover.style.transition = "600ms opacity";
        });

        var initialHintWidth = hintBox.clientWidth;
        var showHint = function(forEl, html) {
            forX = elX(forEl);
            forY = elY(forEl);
            hintBox.innerHTML = html;
            hintBox.style.width = initialHintWidth + "px";
            hintBox.style.left = forX + forEl.clientWidth + 12 + "px";
            if ((elX(hintBox) + hintBox.clientWidth) > document.body.clientWidth) {
                hintBox.style.width = document.body.clientWidth - elX(hintBox) + "px";
            }
            hintBox.style.top = (forY + (hintBox.clientHeight * 1.5)) > document.body.scrollHeight ? document.body.scrollHeight - (hintBox.clientHeight * 1.5) + "px" : forY + "px";
            crel("div", {
                background: "rgba(100, 150, 255,0.04)",
                "position": "absolute",
                "box-shadow": "0 0 22px rgba(100, 150, 255,0.09)",
                "top": "42px",
                "left": "0",
                "right": "0",
                "bottom": "0"
            }, hintBox);
            applyStyles(hintBox, {
                opacity: 1,
                transform: "scale(1,1)"
            });
            if(elY(hintBox) + hintBox.clientHeight > hintBox.parentNode.clientHeight + hintBox.parentNode.scrollTop){
                hintBox.style.top = hintBox.parentNode.clientHeight - (hintBox.clientHeight + 16) + hintBox.parentNode.scrollTop + "px";
            }
            hintElement = forEl;
        };

        var hintTimer = null;
        var hintElement = null;

        var xhr = function(url, callback, cached) {
            if (cached && typeof top.kole.xhrCache[url] != "undefined") {
                setTimeout(function() {
                    callback(top.kole.xhrCache[url]);
                }, 0);
                return {
                    cancel: function() {}
                };
            };
            var canceled = false;
            var req = new XMLHttpRequest();
            var stateChange = function() {
                if (this.readyState == 4) {
                    top.kole.xhrCache[url] = this.responseText;
                    if (!canceled)
                        callback(this.responseText);
                    req.removeEventListener("readystatechange", stateChange);
                }
            };
            req.addEventListener("readystatechange", stateChange, false);
            req.open("GET", url, true);
            req.send();
            return {
                cancel: function() {
                    canceled = true;
                }
            }
        };

        var extractDescription = function(url, callback, cached) {
            return xhr(url, function(response) {
                var tempEl = crel("div");
                tempEl.innerHTML = response;
                var scripts = tempEl.querySelectorAll("script");
                for (var i = scripts.length - 1; i >= 0; i--) scripts[i].parentNode.removeChild(scripts[i]);
                callback(tempEl.querySelectorAll("#description")[0].innerHTML);
            }, cached);
        };

        var cancelLastHintCallback = function() {};

        var cancelHint = function() {
            cancelLastHintCallback();
            if (hintTimer !== null) clearTimeout(hintTimer);
            applyStyles(hintBox, {
                opacity: 0,
                transform: "scale(0.9,0.9)"
            });
        };

        // htmlCallback(done(html)) should return {cancel:function(){...}}
        var setHintTimer = function(el, htmlCallback, ___args) {
            cancelHint();
            // show when both timer AND callback async complete
            var asyncRemaining = 2;
            var hintHtml = "";
            var asyncDone = function() {
                asyncRemaining--;
                if (asyncRemaining == 0) {
                    showHint(el, hintHtml);
                }
            };
            hintTimer = setTimeout(asyncDone, getOption("hintDelay"));
            var cancelLastHintCallback = htmlCallback(function(html) {
                hintHtml = html;
                asyncDone();
            }).cancel;
        };
    } // !top

    if (unsafeWindow == unsafeWindow.top) {
        var whenReady = function(cb) {
            for (var i = 0; i < unsafeWindow.frames.length; i++)
                if (typeof unsafeWindow.frames[i].kole == "undefined") {
                    setTimeout(function() {
                        whenReady(cb);
                    }, 200);
                    return;
                }
            cb();
        }
        if (typeof this.kole != "undefined") {
            alert("A browser plugin or KoL update is conflicting with KoLE");
            return;
        }
        unsafeWindow.kole = null;
        whenReady(function() {
            var GM_itemIds = GM_getValue("itemIds");
            var GM_iconThemes = GM_getValue("iconThemes");
            if(typeof GM_iconThemes != "undefined") GM_iconThemes = JSON.parse(GM_iconThemes);
            if(GM_iconThemes && Object.keys(GM_iconThemes).length == 1 && GM_iconThemes.hasOwnProperty("example")){
                GM_deleteValue("iconThemes");
                GM_iconThemes = undefined;
            }
            unsafeWindow.kole = {
                setDarkness: function(darkness) {
                    for (var i = 0; i < frames.length; i++) {
                        unsafeWindow.frames[i].window.kole.setDarkness(darkness);
                    }
                },
                itemIds: typeof GM_itemIds == "undefined" ? {} : JSON.parse(GM_itemIds),
                iconThemes: typeof GM_iconThemes == "undefined" ? defaultIconThemes : GM_iconThemes,
                xhrCache: {}
            };
        });
    } else {
        unsafeWindow.kole = {
            setDarkness: function(darkness) {
                darkCover.style.opacity = darkness + 0.00001; // fixes gre render bug
            },
            top: top.kole
        };
        unsafeWindow.kole.setDarkness(getOption("darkness"));
    }

    var poop = function(htmlOrElement, onclose) {
        var cover = crel("div", {
            position: "fixed",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            background: "rgba(0,0,0,0.3)",
            opacity: 0,
            transition: "200ms all",
            "z-index": 9002
        });
        var win = crel("div", {
            width: "66vw",
            "min-width": "480px",
            "max-width": "600px",
            margin: "34vh auto 0 auto",
            background: "#eee",
            position: "relative",
            transform: "scale(1,5,1.5) translateY(-30%)",
            "max-height": "78%",
            "overflow": "auto",
            opacity: 0,
            transition: "400ms all",
            "box-shadow": "1px 1px 6px rgba(0,0,0,0.4)"
        }, cover);
        var inner = crel("div", {
            padding: "12px"
        }, win);
        var winner = crel("div", {
            padding: "12px"
        }, inner);
        if (typeof htmlOrElement == "string") {
            winner.innerHTML = htmlOrElement;
        } else {
            winner.appendChild(htmlOrElement)
        }
        var close = function() {
            cover.style.opacity = 0;
            applyStyles(win, {
                "margin-top": "20%",
                opacity: 0
            });
            setTimeout(function() {
                document.body.removeChild(cover);
            }, 600);
            if (onclose) onclose();
        }
        var closeButton = crel("button", {
            position: "absolute",
            top: 0,
            right: 0,
            border: "none",
            background: "#fff",
            "padding": "0 0.8em",
            "font-size": "1.2em"
        }, win);
        applyStyles(closeButton, {
            "background": "#f88"
        }, "hover");
        with(closeButton) {
            onclick = close;
            innerHTML = "&#x2716;";
        }
        document.body.appendChild(cover);
        setTimeout(function() {
            cover.style.opacity = 1;
            applyStyles(win, {
                "transform": "scale(1,1) translateY(-50%)",
                opacity: 1
            });
        }, 100);
        return {
            close: close
        };
    };

    var timedClick = function(el, delay, cancelCaption, oncancel) {
        var cancelButton = crel("button", {
            background: "#fff",
            border: "2px #000 solid",
            transition: delay + "ms box-shadow linear",
            "box-shadow": "inset 0 0 0 rgba(255,0,0,0.9)",
            position: "fixed",
            bottom: 0,
            right: 0,
            padding: "4px",
            width: "200px"
        }, document.body);
        applyStyles(el, {
            transition: delay + "ms box-shadow linear, " + delay + "ms border-color linear",
            "box-shadow": "inset 0 0 0 rgba(0,255,0,0.5)",
            "border-color": "rgba(0,255,0,0)"
        });
        cancelButton.innerHTML = cancelCaption;
        var canceled = false;
        var timer = setTimeout(function() {
            if(!canceled) el.click();
        }, delay + 10);
        setTimeout(function() {
            cancelButton.style.boxShadow = "inset 208px 0 0 rgba(255,0,0,1)";
            el.style.boxShadow = "inset " + el.scrollWidth + "px 0 0 rgba(0,255,0,0.3)";
            el.style.borderColor = "rgba(0,255,0,1)";
        }, 10);
        cancelButton.onclick = function() {
            canceled = true;
            if (oncancel) oncancel();
            document.body.removeChild(cancelButton);
        };
        return timer;
    };

    if (window.name == "mainpane") {
        if (!document) throw new Error("no document");
        if (!document.body) throw new Error("document has no body");
        var quickAFLabel = crel("label", {
            "position": "fixed",
            "top": 0,
            "right": "64px",
            "background": "#eee"
        }, document.body);
        var quickAF = crel("input", {}, quickAFLabel);
        quickAF.id = "quickAF";
        quickAF.checked = getOption("autoFight");
        quickAF.type = "checkbox";
        quickAF.onchange = function(){
            if(!quickAF.checked && autoFightTimer !== null){
                clearTimeout(autoFightTimer);
                autoFightTimer = null;
            }
            setOption("autoFight", quickAF.checked);
        };
        var quickAFText = crel("span", {}, quickAFLabel);
        quickAFText.innerHTML = "Auto Fight";
        
        var watchText = getOption("watchText");
        if(watchText !== ""){
            var watches = watchText.split("|");
            for(var i = 0; i < watches.length; i++){
                if(document.body.innerHTML.indexOf(watches[i]) !== -1){
                    setOption("autoFight", false);
                    quickAF.checked = false;
                    if(!Notification){
                        alert("Text found: " + watches[i]);
                    } else{
                        poop("Text found: " + watches[i]);
                        desktopNotify("[KoLE] Text found: " + watches[i]);
                    }                    
                    break;
                }
            }
        }

       
        var openPanel = function() {
            if (!top.kole) return;
            if (closePanel !== null) {
                console.warn("Panel already open?");
                return;
            }
            koleButton.disabled = true;
            var panel = crel("div", {
                "position": "fixed",
                "top": "0",
                "left": "0",
                "right": "0",
                "max-height": "80%",
                "box-shadow": "0 0 8px rgba(0,0,0,0.6)",
                "border-bottom": "1px #888 solid",
                "transform": "scale(0.1,0.1)",
                "transform-origin": "100% 0",
                "padding": "12px",
                "overflow": "auto",
                "opacity": 0,
                "z-index": 9001,
                "background": "#eef",
                "transition": "320ms all ease-in"
            }, document.body);
            crel("h1", {
                "font-weight": "100"
            }, panel).innerHTML = "KoLE Settings";
            var settingsTable = crel("table", {
                "font-size": "small",
                "width": "100%",
            }, panel);
            settingsTable.setAttribute("cellpadding", 4);
            settingsTable.setAttribute("cellspacing", 0);
            var tbody = crel("tbody", {}, settingsTable);
            var alt = false;
            for (var name in configOptions) {
                alt = !alt;
                var spec = configOptions[name];
                var tr = crel("tr", {
                    background: alt ? "rgba(255,255,255,0.5)" : "transparent"
                }, tbody);
                var labelCell = crel("td", {
                    height: "2em",
                    "width": "200px",
                    "vertical-align": "middle"
                }, tr);
                var label = crel("label", {}, labelCell);
                label.innerHTML = spec.caption;
                label.setAttribute("for", "kole_config_" + name);
                var editCell = crel("td", {
                    "vertical-align": "middle"
                }, tr);
                controlTypes[spec.control](name, spec, editCell);
                if (spec.description) {
                    var descTr = crel("tr", {
                        "font-size": "0.8em",
                        "color": "#666",
                        background: alt ? "rgba(255,255,255,0.5)" : "transparent"
                    }, tbody);
                    var descTd = crel("td", {}, descTr);
                    descTd.innerHTML = spec.description;
                    descTd.setAttribute("colspan", 2);
                }
            }
            with(crel("p", {
                "font-size": "small"
            }, panel)) {
                innerHTML = "Kingdom of Loathing Enhancement <b>alpha</b> by <a href='showplayer.php?who=2362564'>fnoot</a><br>This is an <b>alpha testing</b> version; please report any problems by kmail.";
            }
            var closeButton = crel("button", {
                position: "absolute",
                top: 0,
                right: 0,
                background: "#fff",
                border: "none",
                "font-size": "18px",
                "padding": "0 .8em"
            }, panel);
            applyStyles(closeButton, {
                "background": "#f88"
            }, "hover");
            closeButton.innerHTML = "&#x2716;";
            closePanel = function() {
                closePanel = null;
                koleButton.disabled = false;
                applyStyles(panel, {
                    opacity: 0,
                    "pointer-events": "none",
                    transform: "scale(0.02,0.02)"
                });
                setTimeout(function() {
                    document.body.removeChild(panel);
                    panel = null;
                }, 1000);
            };
            panel.onscroll = function(e){
                applyStyles(closeButton, {top: panel.scrollTop + "px"});
            };
            closeButton.onclick = closePanel;
            setTimeout(function() {
                applyStyles(panel, {
                    opacity: 1,
                    transform: "scale(1,1)"
                });
            }, 100)
        };
        var koleButton = crel("button", {
            "position": "fixed",
            "top": "0",
            "right": "0",
            "z-index": 9000,
            "border": "none",
            "background": "#eee",
            "padding": "3px 6px",
            "transition": "400ms transform ease-out, 300ms opacity ease-out, 400ms 300ms color ease-out, 600ms background ease-out",
            "transform-origin": "100% 0",
            "color": "#000"
        }, document.body);
        applyStyles(koleButton, {
            background: "#adf"
        }, "hover");
        applyStyles(koleButton, {
            transition: "400ms transform ease-in, 300ms opacity ease-in, 200ms color ease-in",
            background: "#adf",
            opacity: 0,
            transform: "scale(6,6)",
            color: "rgba(0,0,0,0)"
        }, "disabled");
        with(koleButton) {
            innerHTML = "KoLE";
            onclick = openPanel;
        }
        
        var autoFightTimer = null;

        if (getOption("autoFight")) {
            (function() { // auto fighting
                if (!top.kole) return;
                var hpPercent = (top.kole.hp / top.kole.maxHp) * 100;
                if (hpPercent < getOption("autoFightMinHp")) {
                    setOption("autoFight", false);
                    return;
                }
                var links = document.querySelectorAll("a");
                var adventureAgainRegex = /Adventure Again \(|Fight Again \(/;
                for (var i = 0; i < links.length; i++) {
                    if (adventureAgainRegex.test(links[i].innerHTML)) {
                        autoFightTimer = timedClick(links[i], getOption("autoFightDelay"), "Cancel automatic fighting", function() {
                            setOption("autoFight", false);
                            quickAF.checked = false;
                        });
                        return;
                    }
                }
                var button12 = document.getElementById("button12");
                if (button12 != null) {
                    autoFightTimer = timedClick(button12, getOption("autoFightDelay"), "Cancel automatic fighting", function() {
                        setOption("autoFight", false);
                        quickAF.checked = false;
                    });
                }
            })();
        }
        if(getOption("autoDaily")){
            var buttons = document.body.querySelectorAll(".bfast");
            if(buttons.length > 0){
               timedClick(buttons[0], 1500, "Cancel Auto Daily", function(){
                  setOption("autoDaily", false);
               });
                setTimeout(function(){
                    location.reload();
                }, 2500);
            }
        }
        
        var autoButtons = GM_getValue("autoButtons");
        if(autoButtons && autoButtons !== "")(function(){
            var buttons = document.querySelectorAll("input[type=submit],value");
            var captions = autoButtons.split(/[\r\n]+/);
            for(var i = 0; i < buttons.length; i++){
                var button = buttons[i];
                var cap = button[button.tagName.toUpperCase() == "INPUT" ? "value" : "innerHTML"];
                if(captions.indexOf(cap) !== -1) timedClick(buttons[i], getOption('autoButtonDelay'), "Cancel Auto-button", function(){
                    GM_setValue("autoButtons", captions.filter(function(caption){ return cap !== caption }).join("\n"));
                });
            }
        })();

        setInterval(function() {
            if (getOption("stayLoggedIn")) {
                xhr("main.php", function() {}, false);
            }
        }, 1000 * 60 * 2);
        var scanItems = function() {
            if (top.kole == null) return;
            var itemIds = top.kole.itemIds;
            var learnedItems = false;
            var itemCells = document.querySelectorAll(".stuffbox table.item .ircm");
            for (var i = 0; i < itemCells.length; i++) {
                var cell = itemCells[i];
                var itemName = cell.innerHTML;
                if (typeof itemIds[itemName] == "undefined") {
                    learnedItems = true;
                    itemIds[itemName] = cell.parentNode.id.replace(/i/, '');
                }
            }
            if (learnedItems) GM_setValue("itemIds", JSON.stringify(itemIds));
        };
        setInterval(scanItems, 3500);

    } // /mainpane

    var chatXhr = function(msg, callback) {
        var url = '/submitnewchat.php?playerid=' + unsafeWindow.playerid + '&pwd=' + unsafeWindow.pwdhash + '&graf=' + encodeURIComponent(msg) + '&j=1';
        xhr(url, function(response) {
            callback(response);
        });
    };

    var getItemCount = function(item, callback) {
        chatXhr("/count " + item, function(res) {
            var countMatches = res.match(/You have (\d+)/);
            if (countMatches && countMatches.length > 0) {
                callback(countMatches[1]);
            } else {
                callback(0);
            }
        });
    };

    var hoverLinks = function() {
        window.addEventListener("load", function() {
            crel("style", {}, document.querySelectorAll("head")[0]).textContent = "a{opacity:0.8;} a:hover{opacity:1;}";
        }, false);
    }

    if (window.name == "menupane") {
        hoverLinks();
        var lastAppliedIconTheme = "";
        var replaceImages = function() {
            var theme;
            if (!unsafeWindow.top.kole) {
                setTimeout(replaceImages, 100);
                return;
            }
            var themeName = getOption("iconTheme");
            if (themeName == lastAppliedIconTheme) return;
            lastAppliedIconTheme = themeName;
            if (themeName == "") {
                theme = {
                    icons: {}
                };
            } else {
                if (typeof unsafeWindow.top.kole.iconThemes[themeName] == "undefined") {
                    setOption("iconTheme", "");
                    console.warn("Can't find icon theme " + themeName);
                    return;
                }
                theme = unsafeWindow.top.kole.iconThemes[themeName];
            }
            var imgs = document.querySelectorAll("img");
            document.body.style.background = typeof theme.bg == "undefined" ? "#fff" : theme.bg;
            for (var i = 0; i < imgs.length; i++) {
                if (typeof imgs[i].originalSrc == "undefined") imgs[i].originalSrc = imgs[i].src;
                var src = imgs[i].originalSrc;
                if (typeof theme.icons[src] != "undefined") {
                    //var width = imgs[i].width;
                    imgs[i].src = theme.icons[src];
                    imgs[i].width = 30;
                } else {
                    imgs[i].src = imgs[i].originalSrc;
                }
            }
        };
        replaceImages();
        setInterval(replaceImages, 500);
    }

    if (window.name == "charpane") {
        hoverLinks();
        var getHp = function() {
            if (!top.kole) {
                setTimeout(getHp, 200);
                return;
            }
            var blacks = document.querySelectorAll(".black");
            for (var i = 0; i < blacks.length; i++) {
                var sib = blacks[i];
                while (sib && sib.tagName.toLowerCase() != "img") {
                    sib = sib.previousSibling;
                }
                if(!sib){
                    // compact charpane?
                    var tr = blacks[i].parentNode.parentNode; // td > tr
                    var tds = tr.querySelectorAll("td");
                    var hptds = [].slice.call(tds, 0).filter(function(td){
                        var imgs = td.querySelectorAll("img");
                        return [].slice.call(imgs, 0).filter(function(img){
                            return img.title == "Hit Points";
                        }).length > 0;
                    });
                    if(hptds.length > 0){
                        var blacks = hptds[0].nextSibling.querySelectorAll(".black");
                        if(blacks.length > 0){
                            var matches = blacks[0].innerHTML.match(/(\d+)&nbsp;\/&nbsp;(\d+)/);
                            top.kole.hp = matches[1];
                            top.kole.maxHp = matches[2];
                            var percent = (top.kole.hp / top.kole.maxHp) * 100;
                            if (percent < getOption("autoFightMinHp")) {
                                blacks[i].style.background = "rgba(255,0,0,0.2)";
                            }
                            return;
                        }
                    }                   
                }
                if (sib && sib.title == "Hit Points") {
                    var matches = blacks[i].innerHTML.match(/(\d+)&nbsp;\/&nbsp;(\d+)/);
                    top.kole.hp = matches[1];
                    top.kole.maxHp = matches[2];
                    var percent = (top.kole.hp / top.kole.maxHp) * 100;
                    if (percent < getOption("autoFightMinHp")) {
                        blacks[i].style.background = "rgba(255,0,0,0.2)";
                    }
                    return;
                }
            }
            top.kole.hp = 1;
            top.kole.maxHp = 1;
            console.warn("[kole] Couldn't find HP");
        };
        getHp();
    }

    if (window.name == "chatpane") {
        hoverLinks();

        var chatLog = function(s) {
            var activeWindow = unsafeWindow.$$('.chatdisplay:visible')[0];
            var toscroll = (activeWindow.scrollHeight - (activeWindow.scrollTop + activeWindow.offsetHeight) < 4);
            var msg = crel("div", {
                color: "#090"
            });
            msg.innerHTML = "<span style='color:#0c0'><span style='opacity:0.4'>[</span>kole<span style='opacity:0.4'>]</span></span> " + s;
            activeWindow.appendChild(msg);
            if (toscroll) {
                activeWindow.scrollTop = activeWindow.scrollHeight;
            }
        };
        var chatCommands = {
            wiki: function(args) {
                window.open("http://kol.coldfront.net/thekolwiki/index.php/Special:Search?search=" + encodeURIComponent(args.trim()) + "&go=Go");
            },
            qs: function(args) {
                args = args.trim();
                var amountMatches = args.match(/(\d+|\*)\s+([\w\s]+)/);
                if (amountMatches && amountMatches.length > 1) {
                    var amount = amountMatches[1];
                    args = amountMatches[2];
                } else {
                    var amount = 1;
                }
                var item = itemLookup(args);
                if (item === null) {
                    chatLog("KoLE doesn't know that item's ID! Teach it by opening your inventory.");
                } else if (item.error) {
                    chatLog("" + item.error);
                } else {
                    if (amount == "*") {
                        chatLog("Retrieving item count");
                        getItemCount(item.name, function(count) {
                            if (count == 0) {
                                chatLog("You don't have any of those!");
                            }
                            chatLog("Quickselling " + count + " x " + item.name);
                            unsafeWindow.dojax("sellstuff.php?action=sell&ajax=1&type=quant&whichitem%5B%5D=" + item.id + "&howmany=" + count + "&pwd=" + unsafeWindow.pwdhash);
                        });
                    } else {
                        chatLog("Quickselling " + amount + " x " + item.name);
                        unsafeWindow.dojax("sellstuff.php?action=sell&ajax=1&type=quant&whichitem%5B%5D=" + item.id + "&howmany=" + amount + "&pwd=" + unsafeWindow.pwdhash);
                    }
                }
            },
            jump: function(args) {
                chatLog("Weeeeeeeee");
            }
        }

        var previousOnload = window.onload;
        window.onload = function() {
            if (previousOnload) previousOnload.apply(this, arguments);
            var oldForm = document.getElementById('InputForm');
            if (oldForm == null) return;
            oldForm = oldForm.parentNode;
            newForm = oldForm.cloneNode(true);
            newForm.style.background = "red";
            oldForm.parentNode.replaceChild(newForm, oldForm);
            var $inp = unsafeWindow.$$("#graf");
            unsafeWindow.$inp = $inp;
            newForm.onsubmit = function(ev) {
                ev.preventDefault();
                var inp = $inp.val();
                var matches = inp.match(/^\/(\w+)(\s+(.*))?/);
                if (matches && matches.length > 0) {
                    var cmd = matches[1];
                    if (typeof chatCommands[cmd] != "undefined") {
                        $inp.val("");
                        chatCommands[cmd].call(this, matches.length > 1 ? matches[2] : undefined);
                        return;
                    }
                }
                $inp.val(getOption("nullifySword") ? nullifySword($inp.val()) : $inp.val());
                unsafeWindow.submitchat(ev);
            };
        };
    };

    var applyHoverHints = function() {
        var els = document.querySelectorAll("a,img");
        var onclickRegex = /\b(descitem|eff|javascript:poop)\("?([\w\.\?\=]+)"?\b(\s*,\s*(\w+)\b\))?/;
        for (var i = 0; i < els.length; i++) {
            var funcUrls = {
                descitem: "desc_item.php?whichitem=",
                eff: "desc_effect.php?whicheffect=",
                "javascript:poop": ""
            };
            var onclick = els[i].getAttribute("onclick") + "";
            var matches = onclick.match(onclickRegex);
            if (onclick && matches && (matches.length > 0)) {
                if (typeof els[i]['@kole2_hoverhint_init'] == "undefined") {
                    els[i]['@kole2_hoverhint_init'] = true;
                    els[i].style.cursor = "help";
                    els[i].title = "";
                    with({
                            func: matches[1],
                            itemId: matches[2],
                            otherPlayer: matches[4]
                        }) {
                            els[i].addEventListener("mouseenter", function() {
                                cancelHint();
                                if (!getOption("hoverHints")) return;
                                hintElement = this;
                                var query = typeof otherPlayer == "undefined" ? itemId : itemId + "&otherplayer=" + otherPlayer;
                                setHintTimer(this, function(callback) {
                                    return extractDescription(funcUrls[func] + query, callback, true);
                                });
                            }, false);
                            els[i].addEventListener("mouseout", function() {
                                if (this == hintElement) cancelHint();
                            }, false);
                        } // with
                } // if not init
            } // if onclick match
        } // for i in els
    }; // applyHoverHints()

    if (window !== top) {
        applyHoverHints();
        setInterval(applyHoverHints, 2500);
    }

    var editIconTheme = function(name, ondone) {
        var startingTheme = getOption("iconTheme");
        var blankSrc = "http://images.kingdomofloathing.com/pixel.gif";
        // temporarily disable theme so Copy Link Location gets the correct URL
        setOption("iconTheme", "");
        //		top.kole.userSettings.iconTheme = "";
        var theme = name == "" ? {
            displayName: "New Theme",
            icons: {}
        } : top.kole.iconThemes[name];
        if (typeof theme == "undefined") theme = {
            displayName: "",
            icons: {}
        };
        var editor = crel("div", {});
        var tableRows = [
            ["<b>Original Image Location</b>", "<b>Icon</b>"]
        ];

        var createImg = function(src) {
            var img = crel("img", {
                cursor: "pointer",
                "vertical-align": "middle"
            });
            img.width = 30;
            img.height = 30;
            img.src = src;
            img.onclick = function() {
                var fileSelect = crel("input");
                var img = this;
                fileSelect.setAttribute("type", "file");
                fileSelect.onchange = function(ev) {
                    var reader = new FileReader();
                    reader.onloadend = function() {
                        img.src = reader.result;
                    };
                    var file = this.files[0];
                    reader.readAsDataURL(file)
                };
                fileSelect.click();
            };
            return img;
        };
        var createInput = function(value) {
            var input = crel("input", {
                width: '340px',
                "font-size": "0.8em"
            });
            input.value = value;
            return input;
        };
        var createDeleteButton = function() {
            var deleteButton = crel("button", {
                border: "none"
            });
            deleteButton.innerHTML = "X";
            deleteButton.onclick = function() {
                if (!confirm("Delete this icon?")) return;
                var row = this.parentNode.parentNode.parentNode;
                row.parentNode.removeChild(row);
                reapplyAlt();
            };
            return deleteButton;
        };

        for (var orig in theme.icons) {
            var right = crel("div");
            right.appendChild(createImg(theme.icons[orig]));
            right.appendChild(createDeleteButton());
            tableRows.push([createInput(orig), right]);
        }

        var table = tabulate(tableRows);
        var reapplyAlt = function() {
            var trs = table.querySelectorAll("tr");
            for (var i = 0; i < trs.length; i++) trs[i].style.background = i % 2 == 1 ? "rgba(0,0,0,0.05)" : "transparent";
        };
        reapplyAlt();
        crel("h1", {
            "font-weight": 100,
            margin: "0 0 12px 0"
        }, editor).innerHTML = "Editing " + theme.displayName;
        var uniqueNameInput = crel("input", {
            width: "200px"
        });
        uniqueNameInput.value = name;
        var displayNameInput = crel("input", {
            width: "200px"
        });
        displayNameInput.value = theme.displayName;
        editor.appendChild(tabulate([
            ["Unique name", uniqueNameInput],
            ["Display name", displayNameInput]
        ], "rgba(0,0,0,0.05)"));


        editor.appendChild(table);
        var addButton = crel("button", {
            "float": "right"
        }, editor);
        addButton.innerHTML = "+";
        addButton.onclick = function() {
            var tr = crel("tr", {}, table);
            var tdl = crel("td", {}, tr);
            var tdr = crel("td", {}, tr);
            var right = crel("div", {}, tdr);
            tdl.appendChild(createInput(""));
            right.appendChild(createImg(blankSrc));
            right.appendChild(createDeleteButton());
            table.appendChild(tr);
        };

        var p = crel("p", {}, editor);
        p.innerHTML = "Click an image to select a replacement. ";
        var helpLink = crel("a", {}, p);
        helpLink.innerHTML = "Help";
        helpLink.href = "#";
        helpLink.onclick = function() {
            poop("<h1 style='font-weight:100; margin:0 0 12px 0'>Theme Editor Help</h1><ol style='font-size:small'>" + "<li>Click the + button to add replacements</li>" + "<li>Right-click on a topbar icon and choose 'Copy Image Location'</li>" + "<li>Paste into the input on the left</li>" + "<li>Left-click on the icon on the right to choose a replacement</li>" + "<li>Give your theme a unique name and a fancy display name</li>" + "<li>????</li>" + "<li>Profit!</li>" + "</ol>" + "<p>Unique names are like filenames. If you change it to something new you'll create a new theme. If you change it to something that exists, you'll overwrite it.</p>");
            return false;
        };

        var saveButton = crel("Button", {}, editor);
        var saved = false;
        saveButton.innerHTML = "Save";
        saveButton.onclick = function() {
            if (uniqueNameInput.value.trim() == "") {
                poop("Please supply a unique name");
                return;
            }
            if (displayNameInput.value.trim() == "") {
                poop("Please supply a display name");
                return;
            }
            var rows = table.querySelectorAll("tr");
            var theme = {
                displayName: displayNameInput.value,
                icons: {}
            };
            for (var i = 1; i < rows.length; i++) {
                var orig = rows[i].querySelectorAll("input")[0].value;
                var repl = rows[i].querySelectorAll("img")[0].src;
                if (orig && repl != blankSrc) {
                    theme.icons[orig] = repl;
                }
            }
            top.kole.iconThemes[uniqueNameInput.value] = theme;
            saveIconThemes();
            saved = true;
            editorPoop.close();
        };

        var editorPoop = poop(editor, function() {
            if (saved) {
                if (uniqueNameInput.value != startingTheme) {
                    setOption("iconTheme", startingTheme);
                }
                if (closePanel) closePanel();
                openPanel();
            } else {
                setOption("iconTheme", startingTheme);
            }
            if (ondone) ondone();
        });
    };

    var exportIconTheme = function(name) {
        var theme = top.kole.iconThemes[name];
        if (!theme) throw new Error("No such theme: " + name);
        var exported = {
            displayName: theme.displayName,
            uniqueName: name,
            icons: theme.icons
        };
        return JSON.stringify(exported);
    };

    var importIconTheme = function(onclose) {
        var pop = crel("div");
        crel("h1", {
            margin: "0 0 12px 0",
            "font-weight": "100"
        }, pop).innerHTML = "Import Icon Theme";
        var textarea = crel("textarea", {
            "width": "100%",
            height: "200px",
            background: "#fff"
        }, pop);
        textarea.placeholder = "Paste the theme code here";
        var button = crel("button", {}, pop);
        button.innerHTML = "Import";
        button.onclick = function() {
            try {
                if (textarea.value.trim().substr(0, 1) !== "{") throw new Error("Import data must start with {");
                var theme = JSON.parse(textarea.value);
                var name = theme.uniqueName;
                if (!theme.icons) throw new error("Missing theme.icons");
                top.kole.iconThemes[name] = theme;
                saveIconThemes();
                importPoop.close();
                if (name == getOption("iconTheme")) {
                    // current theme was changed - reset current to vanilla to clear menupane's cache
                    // alternatively, cache by theme obj rather than name?
                    top.kol.userSettings.iconTheme = "";
                }
                if (closePanel) {
                    closePanel();
                    openPanel();
                }
            } catch (ex) {
                console.warn(ex.message);
                poop("Oops! That theme code doesn't seem quite right!<br><code style='font-size:0.8em'>" + ex.message + "</code>");
                return;
            }
        };
        var importPoop = poop(pop, onclose);
    };

})();